Passed
Push — master ( 34e8da...f497d2 )
by
unknown
06:10 queued 02:50
created
version.php 1 patch
Indentation   +9 added lines, -9 removed lines patch added patch discarded remove patch
@@ -6,13 +6,13 @@
 block discarded – undo
6 6
  */
7 7
 
8 8
 if (!defined("GROMMUNIOSYNC_VERSION")) {
9
-	$path = escapeshellarg(dirname(realpath($_SERVER['SCRIPT_FILENAME'])));
10
-	$branch = trim(exec("hash git 2>/dev/null && cd {$path} >/dev/null 2>&1 && git branch --no-color 2>/dev/null | sed -e '/^[^*]/d' -e \"s/* \\(.*\\)/\\1/\""));
11
-	$version = exec("hash git 2>/dev/null && cd {$path} >/dev/null 2>&1 && git describe  --always 2>/dev/null");
12
-	if ($branch && $version) {
13
-		define("GROMMUNIOSYNC_VERSION", $branch . '-' . $version);
14
-	}
15
-	else {
16
-		define("GROMMUNIOSYNC_VERSION", "GIT");
17
-	}
9
+    $path = escapeshellarg(dirname(realpath($_SERVER['SCRIPT_FILENAME'])));
10
+    $branch = trim(exec("hash git 2>/dev/null && cd {$path} >/dev/null 2>&1 && git branch --no-color 2>/dev/null | sed -e '/^[^*]/d' -e \"s/* \\(.*\\)/\\1/\""));
11
+    $version = exec("hash git 2>/dev/null && cd {$path} >/dev/null 2>&1 && git describe  --always 2>/dev/null");
12
+    if ($branch && $version) {
13
+        define("GROMMUNIOSYNC_VERSION", $branch . '-' . $version);
14
+    }
15
+    else {
16
+        define("GROMMUNIOSYNC_VERSION", "GIT");
17
+    }
18 18
 }
Please login to merge, or discard this patch.
mapi/class.meetingrequest.php 1 patch
Indentation   +3796 added lines, -3796 removed lines patch added patch discarded remove patch
@@ -6,7 +6,7 @@  discard block
 block discarded – undo
6 6
  */
7 7
 
8 8
 class Meetingrequest {
9
-	/*
9
+    /*
10 10
 	 * NOTE
11 11
 	 *
12 12
 	 * This class is designed to modify and update meeting request properties
@@ -22,7 +22,7 @@  discard block
 block discarded – undo
22 22
 	 *
23 23
 	 */
24 24
 
25
-	/*
25
+    /*
26 26
 	 * How to use
27 27
 	 * ----------
28 28
 	 *
@@ -73,666 +73,666 @@  discard block
 block discarded – undo
73 73
 	 *     meeting object from calendar
74 74
 	 */
75 75
 
76
-	// All properties for a recipient that are interesting
77
-	public $recipprops = [
78
-		PR_ENTRYID,
79
-		PR_DISPLAY_NAME,
80
-		PR_EMAIL_ADDRESS,
81
-		PR_RECIPIENT_ENTRYID,
82
-		PR_RECIPIENT_TYPE,
83
-		PR_SEND_INTERNET_ENCODING,
84
-		PR_SEND_RICH_INFO,
85
-		PR_RECIPIENT_DISPLAY_NAME,
86
-		PR_ADDRTYPE,
87
-		PR_DISPLAY_TYPE,
88
-		PR_DISPLAY_TYPE_EX,
89
-		PR_RECIPIENT_TRACKSTATUS,
90
-		PR_RECIPIENT_TRACKSTATUS_TIME,
91
-		PR_RECIPIENT_FLAGS,
92
-		PR_ROWID,
93
-		PR_OBJECT_TYPE,
94
-		PR_SEARCH_KEY,
95
-	];
96
-
97
-	/**
98
-	 * Indication whether the setting of resources in a Meeting Request is success (false) or if it
99
-	 * has failed (integer).
100
-	 */
101
-	public $errorSetResource;
102
-
103
-	private $proptags;
104
-	private $store;
105
-	private $message;
106
-	private $session;
107
-	private $meetingTimeInfo;
108
-	private $enableDirectBooking;
109
-	private $includesResources;
110
-	private $nonAcceptingResources;
111
-	private $recipientDisplayname;
112
-
113
-	/**
114
-	 * Constructor.
115
-	 *
116
-	 * Takes a store and a message. The message is an appointment item
117
-	 * that should be converted into a meeting request or an incoming
118
-	 * e-mail message that is a meeting request.
119
-	 *
120
-	 * The $session variable is optional, but required if the following features
121
-	 * are to be used:
122
-	 *
123
-	 * - Sending meeting requests for meetings that are not in your own store
124
-	 * - Sending meeting requests to resources, resource availability checking and resource freebusy updates
125
-	 *
126
-	 * @param mixed $store
127
-	 * @param mixed $message
128
-	 * @param mixed $session
129
-	 * @param mixed $enableDirectBooking
130
-	 */
131
-	public function __construct($store, $message, $session = false, $enableDirectBooking = true) {
132
-		$this->store = $store;
133
-		$this->message = $message;
134
-		$this->session = $session;
135
-		// This variable string saves time information for the MR.
136
-		$this->meetingTimeInfo = false;
137
-		$this->enableDirectBooking = $enableDirectBooking;
138
-
139
-		$properties['goid'] = 'PT_BINARY:PSETID_Meeting:0x3';
140
-		$properties['goid2'] = 'PT_BINARY:PSETID_Meeting:0x23';
141
-		$properties['type'] = 'PT_STRING8:PSETID_Meeting:0x24';
142
-		$properties['meetingrecurring'] = 'PT_BOOLEAN:PSETID_Meeting:0x5';
143
-		$properties['unknown2'] = 'PT_BOOLEAN:PSETID_Meeting:0xa';
144
-		$properties['attendee_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1';
145
-		$properties['owner_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1a';
146
-		$properties['meetingstatus'] = 'PT_LONG:PSETID_Appointment:0x8217';
147
-		$properties['responsestatus'] = 'PT_LONG:PSETID_Appointment:0x8218';
148
-		$properties['unknown6'] = 'PT_LONG:PSETID_Meeting:0x4';
149
-		$properties['replytime'] = 'PT_SYSTIME:PSETID_Appointment:0x8220';
150
-		$properties['usetnef'] = 'PT_BOOLEAN:PSETID_Common:0x8582';
151
-		$properties['recurrence_data'] = 'PT_BINARY:PSETID_Appointment:0x8216';
152
-		$properties['reminderminutes'] = 'PT_LONG:PSETID_Common:0x8501';
153
-		$properties['reminderset'] = 'PT_BOOLEAN:PSETID_Common:0x8503';
154
-		$properties['sendasical'] = 'PT_BOOLEAN:PSETID_Appointment:0x8200';
155
-		$properties['updatecounter'] = 'PT_LONG:PSETID_Appointment:0x8201';                    // AppointmentSequenceNumber
156
-		$properties['last_updatecounter'] = 'PT_LONG:PSETID_Appointment:0x8203';            // AppointmentLastSequence
157
-		$properties['unknown7'] = 'PT_LONG:PSETID_Appointment:0x8202';
158
-		$properties['busystatus'] = 'PT_LONG:PSETID_Appointment:0x8205';
159
-		$properties['intendedbusystatus'] = 'PT_LONG:PSETID_Appointment:0x8224';
160
-		$properties['start'] = 'PT_SYSTIME:PSETID_Appointment:0x820d';
161
-		$properties['responselocation'] = 'PT_STRING8:PSETID_Meeting:0x2';
162
-		$properties['location'] = 'PT_STRING8:PSETID_Appointment:0x8208';
163
-		$properties['requestsent'] = 'PT_BOOLEAN:PSETID_Appointment:0x8229';        // PidLidFInvited, MeetingRequestWasSent
164
-		$properties['startdate'] = 'PT_SYSTIME:PSETID_Appointment:0x820d';
165
-		$properties['duedate'] = 'PT_SYSTIME:PSETID_Appointment:0x820e';
166
-		$properties['flagdueby'] = 'PT_SYSTIME:PSETID_Common:0x8560';
167
-		$properties['commonstart'] = 'PT_SYSTIME:PSETID_Common:0x8516';
168
-		$properties['commonend'] = 'PT_SYSTIME:PSETID_Common:0x8517';
169
-		$properties['recurring'] = 'PT_BOOLEAN:PSETID_Appointment:0x8223';
170
-		$properties['clipstart'] = 'PT_SYSTIME:PSETID_Appointment:0x8235';
171
-		$properties['clipend'] = 'PT_SYSTIME:PSETID_Appointment:0x8236';
172
-		$properties['start_recur_date'] = 'PT_LONG:PSETID_Meeting:0xD';                // StartRecurTime
173
-		$properties['start_recur_time'] = 'PT_LONG:PSETID_Meeting:0xE';                // StartRecurTime
174
-		$properties['end_recur_date'] = 'PT_LONG:PSETID_Meeting:0xF';                // EndRecurDate
175
-		$properties['end_recur_time'] = 'PT_LONG:PSETID_Meeting:0x10';                // EndRecurTime
176
-		$properties['is_exception'] = 'PT_BOOLEAN:PSETID_Meeting:0xA';                // LID_IS_EXCEPTION
177
-		$properties['apptreplyname'] = 'PT_STRING8:PSETID_Appointment:0x8230';
178
-		// Propose new time properties
179
-		$properties['proposed_start_whole'] = 'PT_SYSTIME:PSETID_Appointment:0x8250';
180
-		$properties['proposed_end_whole'] = 'PT_SYSTIME:PSETID_Appointment:0x8251';
181
-		$properties['proposed_duration'] = 'PT_LONG:PSETID_Appointment:0x8256';
182
-		$properties['counter_proposal'] = 'PT_BOOLEAN:PSETID_Appointment:0x8257';
183
-		$properties['recurring_pattern'] = 'PT_STRING8:PSETID_Appointment:0x8232';
184
-		$properties['basedate'] = 'PT_SYSTIME:PSETID_Appointment:0x8228';
185
-		$properties['meetingtype'] = 'PT_LONG:PSETID_Meeting:0x26';
186
-		$properties['timezone_data'] = 'PT_BINARY:PSETID_Appointment:0x8233';
187
-		$properties['timezone'] = 'PT_STRING8:PSETID_Appointment:0x8234';
188
-		$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
189
-		$properties['toattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823B';
190
-		$properties['ccattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823C';
191
-
192
-		$this->proptags = getPropIdsFromStrings($store, $properties);
193
-	}
194
-
195
-	/**
196
-	 * Sets the direct booking property. This is an alternative to the setting of the direct booking
197
-	 * property through the constructor. However, setting it in the constructor is preferred.
198
-	 *
199
-	 * @param bool $directBookingSetting
200
-	 */
201
-	public function setDirectBooking($directBookingSetting) {
202
-		$this->enableDirectBooking = $directBookingSetting;
203
-	}
204
-
205
-	/**
206
-	 * Returns TRUE if the message pointed to is an incoming meeting request and should
207
-	 * therefore be replied to with doAccept or doDecline().
208
-	 *
209
-	 * @param string $messageClass message class to use for checking
210
-	 *
211
-	 * @return bool returns true if this is a meeting request else false
212
-	 */
213
-	public function isMeetingRequest($messageClass = false) {
214
-		if ($messageClass === false) {
215
-			$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
216
-			$messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
217
-		}
218
-
219
-		if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.request') === 0) {
220
-			return true;
221
-		}
222
-
223
-		return false;
224
-	}
225
-
226
-	/**
227
-	 * Returns TRUE if the message pointed to is a returning meeting request response.
228
-	 *
229
-	 * @param string $messageClass message class to use for checking
230
-	 *
231
-	 * @return bool returns true if this is a meeting request else false
232
-	 */
233
-	public function isMeetingRequestResponse($messageClass = false) {
234
-		if ($messageClass === false) {
235
-			$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
236
-			$messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
237
-		}
238
-
239
-		if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.resp') === 0) {
240
-			return true;
241
-		}
242
-
243
-		return false;
244
-	}
245
-
246
-	/**
247
-	 * Returns TRUE if the message pointed to is a cancellation request.
248
-	 *
249
-	 * @param 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($messageClass = false) {
254
-		if ($messageClass === false) {
255
-			$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
256
-			$messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
257
-		}
258
-
259
-		if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.canceled') === 0) {
260
-			return true;
261
-		}
262
-
263
-		return false;
264
-	}
265
-
266
-	/**
267
-	 * Function is used to get the last update counter of meeting request.
268
-	 *
269
-	 * @return bool|Number false when last_updatecounter not found else return last_updatecounter
270
-	 */
271
-	public function getLastUpdateCounter() {
272
-		$calendarItemProps = mapi_getprops($this->message, [$this->proptags['last_updatecounter']]);
273
-		if (isset($calendarItemProps) && !empty($calendarItemProps)) {
274
-			return $calendarItemProps[$this->proptags['last_updatecounter']];
275
-		}
276
-
277
-		return false;
278
-	}
279
-
280
-	/**
281
-	 * Process an incoming meeting request response. This updates the appointment
282
-	 * in your calendar to show whether the user has accepted or declined.
283
-	 */
284
-	public function processMeetingRequestResponse() {
285
-		if (!$this->isMeetingRequestResponse()) {
286
-			return;
287
-		}
288
-
289
-		if (!$this->isLocalOrganiser()) {
290
-			return;
291
-		}
292
-
293
-		// Get information we need from the response message
294
-		$messageprops = mapi_getprops($this->message, [
295
-			$this->proptags['goid'],
296
-			$this->proptags['goid2'],
297
-			PR_OWNER_APPT_ID,
298
-			PR_SENT_REPRESENTING_EMAIL_ADDRESS,
299
-			PR_SENT_REPRESENTING_NAME,
300
-			PR_SENT_REPRESENTING_ADDRTYPE,
301
-			PR_SENT_REPRESENTING_ENTRYID,
302
-			PR_SENT_REPRESENTING_SEARCH_KEY,
303
-			PR_MESSAGE_DELIVERY_TIME,
304
-			PR_MESSAGE_CLASS,
305
-			PR_PROCESSED,
306
-			PR_RCVD_REPRESENTING_ENTRYID,
307
-			$this->proptags['proposed_start_whole'],
308
-			$this->proptags['proposed_end_whole'],
309
-			$this->proptags['proposed_duration'],
310
-			$this->proptags['counter_proposal'],
311
-			$this->proptags['attendee_critical_change'],
312
-		]);
313
-
314
-		$goid2 = $messageprops[$this->proptags['goid2']];
315
-
316
-		if (!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS])) {
317
-			return;
318
-		}
319
-
320
-		// Find basedate in GlobalID(0x3), this can be a response for an occurrence
321
-		$basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
322
-
323
-		// check if delegate is processing the response
324
-		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
325
-			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
326
-
327
-			$userStore = $delegatorStore['store'];
328
-			$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
329
-		}
330
-		else {
331
-			$userStore = $this->store;
332
-			$calFolder = $this->openDefaultCalendar();
333
-		}
334
-
335
-		// check for calendar access
336
-		if ($this->checkCalendarWriteAccess($userStore) !== true) {
337
-			// Throw an exception that we don't have write permissions on calendar folder,
338
-			// allow caller to fill the error message
339
-			throw new MAPIException(null, MAPI_E_NO_ACCESS);
340
-		}
341
-
342
-		$calendarItem = $this->getCorrespondentCalendarItem(true);
343
-
344
-		// Open the calendar items, and update all the recipients of the calendar item that match
345
-		// the email address of the response.
346
-		if ($calendarItem !== false) {
347
-			$this->processResponse($userStore, $calendarItem, $basedate, $messageprops);
348
-		}
349
-	}
350
-
351
-	/**
352
-	 * Process every incoming MeetingRequest response.This updates the appointment
353
-	 * in your calendar to show whether the user has accepted or declined.
354
-	 *
355
-	 * @param resource    $store        contains the userStore in which the meeting is created
356
-	 * @param MAPIMessage $calendarItem resource of the calendar item for which this response has arrived
357
-	 * @param bool        $basedate     if present the create an exception
358
-	 * @param array       $messageprops contains message properties
359
-	 */
360
-	public function processResponse($store, $calendarItem, $basedate, $messageprops) {
361
-		$senderentryid = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
362
-		$messageclass = $messageprops[PR_MESSAGE_CLASS];
363
-		$deliverytime = $messageprops[PR_MESSAGE_DELIVERY_TIME];
364
-
365
-		// Open the calendar item, find the sender in the recipient table and update all the recipients of the calendar item that match
366
-		// the email address of the response.
367
-		$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring'], PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $this->proptags['updatecounter']]);
368
-
369
-		// check if meeting response is already processed
370
-		if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
371
-			// meeting is already processed
372
-			return;
373
-		}
374
-		mapi_setprops($this->message, [PR_PROCESSED => true]);
375
-		mapi_savechanges($this->message);
376
-
377
-		// if meeting is updated in organizer's calendar then we don't need to process
378
-		// old response
379
-		if ($this->isMeetingUpdated($basedate)) {
380
-			return;
381
-		}
382
-
383
-		// If basedate is found, then create/modify exception msg and do processing
384
-		if ($basedate && isset($calendarItemProps[$this->proptags['recurring']]) && $calendarItemProps[$this->proptags['recurring']] === true) {
385
-			$recurr = new Recurrence($store, $calendarItem);
386
-
387
-			// Copy properties from meeting request
388
-			$exception_props = mapi_getprops($this->message, [
389
-				PR_OWNER_APPT_ID,
390
-				$this->proptags['proposed_start_whole'],
391
-				$this->proptags['proposed_end_whole'],
392
-				$this->proptags['proposed_duration'],
393
-				$this->proptags['counter_proposal'],
394
-			]);
395
-
396
-			// Create/modify exception
397
-			if ($recurr->isException($basedate)) {
398
-				$recurr->modifyException($exception_props, $basedate);
399
-			}
400
-			else {
401
-				// When we are creating an exception we need copy recipients from main recurring item
402
-				$recipTable = mapi_message_getrecipienttable($calendarItem);
403
-				$recips = mapi_table_queryallrows($recipTable, $this->recipprops);
404
-
405
-				// Retrieve actual start/due dates from calendar item.
406
-				$exception_props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
407
-				$exception_props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
408
-
409
-				$recurr->createException($exception_props, $basedate, false, $recips);
410
-			}
411
-
412
-			mapi_savechanges($calendarItem);
413
-
414
-			$attach = $recurr->getExceptionAttachment($basedate);
415
-			if ($attach) {
416
-				$recurringItem = $calendarItem;
417
-				$calendarItem = mapi_attach_openobj($attach, MAPI_MODIFY);
418
-			}
419
-			else {
420
-				return false;
421
-			}
422
-		}
423
-
424
-		// Get the recipients of the calendar item
425
-		$reciptable = mapi_message_getrecipienttable($calendarItem);
426
-		$recipients = mapi_table_queryallrows($reciptable, $this->recipprops);
427
-
428
-		// FIXME we should look at the updatecounter property and compare it
429
-		// to the counter in the recipient to see if this update is actually
430
-		// newer than the status in the calendar item
431
-		$found = false;
432
-
433
-		$totalrecips = 0;
434
-		$acceptedrecips = 0;
435
-		foreach ($recipients as $recipient) {
436
-			++$totalrecips;
437
-			if (isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID], $senderentryid)) {
438
-				$found = true;
439
-
440
-				/*
76
+    // All properties for a recipient that are interesting
77
+    public $recipprops = [
78
+        PR_ENTRYID,
79
+        PR_DISPLAY_NAME,
80
+        PR_EMAIL_ADDRESS,
81
+        PR_RECIPIENT_ENTRYID,
82
+        PR_RECIPIENT_TYPE,
83
+        PR_SEND_INTERNET_ENCODING,
84
+        PR_SEND_RICH_INFO,
85
+        PR_RECIPIENT_DISPLAY_NAME,
86
+        PR_ADDRTYPE,
87
+        PR_DISPLAY_TYPE,
88
+        PR_DISPLAY_TYPE_EX,
89
+        PR_RECIPIENT_TRACKSTATUS,
90
+        PR_RECIPIENT_TRACKSTATUS_TIME,
91
+        PR_RECIPIENT_FLAGS,
92
+        PR_ROWID,
93
+        PR_OBJECT_TYPE,
94
+        PR_SEARCH_KEY,
95
+    ];
96
+
97
+    /**
98
+     * Indication whether the setting of resources in a Meeting Request is success (false) or if it
99
+     * has failed (integer).
100
+     */
101
+    public $errorSetResource;
102
+
103
+    private $proptags;
104
+    private $store;
105
+    private $message;
106
+    private $session;
107
+    private $meetingTimeInfo;
108
+    private $enableDirectBooking;
109
+    private $includesResources;
110
+    private $nonAcceptingResources;
111
+    private $recipientDisplayname;
112
+
113
+    /**
114
+     * Constructor.
115
+     *
116
+     * Takes a store and a message. The message is an appointment item
117
+     * that should be converted into a meeting request or an incoming
118
+     * e-mail message that is a meeting request.
119
+     *
120
+     * The $session variable is optional, but required if the following features
121
+     * are to be used:
122
+     *
123
+     * - Sending meeting requests for meetings that are not in your own store
124
+     * - Sending meeting requests to resources, resource availability checking and resource freebusy updates
125
+     *
126
+     * @param mixed $store
127
+     * @param mixed $message
128
+     * @param mixed $session
129
+     * @param mixed $enableDirectBooking
130
+     */
131
+    public function __construct($store, $message, $session = false, $enableDirectBooking = true) {
132
+        $this->store = $store;
133
+        $this->message = $message;
134
+        $this->session = $session;
135
+        // This variable string saves time information for the MR.
136
+        $this->meetingTimeInfo = false;
137
+        $this->enableDirectBooking = $enableDirectBooking;
138
+
139
+        $properties['goid'] = 'PT_BINARY:PSETID_Meeting:0x3';
140
+        $properties['goid2'] = 'PT_BINARY:PSETID_Meeting:0x23';
141
+        $properties['type'] = 'PT_STRING8:PSETID_Meeting:0x24';
142
+        $properties['meetingrecurring'] = 'PT_BOOLEAN:PSETID_Meeting:0x5';
143
+        $properties['unknown2'] = 'PT_BOOLEAN:PSETID_Meeting:0xa';
144
+        $properties['attendee_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1';
145
+        $properties['owner_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1a';
146
+        $properties['meetingstatus'] = 'PT_LONG:PSETID_Appointment:0x8217';
147
+        $properties['responsestatus'] = 'PT_LONG:PSETID_Appointment:0x8218';
148
+        $properties['unknown6'] = 'PT_LONG:PSETID_Meeting:0x4';
149
+        $properties['replytime'] = 'PT_SYSTIME:PSETID_Appointment:0x8220';
150
+        $properties['usetnef'] = 'PT_BOOLEAN:PSETID_Common:0x8582';
151
+        $properties['recurrence_data'] = 'PT_BINARY:PSETID_Appointment:0x8216';
152
+        $properties['reminderminutes'] = 'PT_LONG:PSETID_Common:0x8501';
153
+        $properties['reminderset'] = 'PT_BOOLEAN:PSETID_Common:0x8503';
154
+        $properties['sendasical'] = 'PT_BOOLEAN:PSETID_Appointment:0x8200';
155
+        $properties['updatecounter'] = 'PT_LONG:PSETID_Appointment:0x8201';                    // AppointmentSequenceNumber
156
+        $properties['last_updatecounter'] = 'PT_LONG:PSETID_Appointment:0x8203';            // AppointmentLastSequence
157
+        $properties['unknown7'] = 'PT_LONG:PSETID_Appointment:0x8202';
158
+        $properties['busystatus'] = 'PT_LONG:PSETID_Appointment:0x8205';
159
+        $properties['intendedbusystatus'] = 'PT_LONG:PSETID_Appointment:0x8224';
160
+        $properties['start'] = 'PT_SYSTIME:PSETID_Appointment:0x820d';
161
+        $properties['responselocation'] = 'PT_STRING8:PSETID_Meeting:0x2';
162
+        $properties['location'] = 'PT_STRING8:PSETID_Appointment:0x8208';
163
+        $properties['requestsent'] = 'PT_BOOLEAN:PSETID_Appointment:0x8229';        // PidLidFInvited, MeetingRequestWasSent
164
+        $properties['startdate'] = 'PT_SYSTIME:PSETID_Appointment:0x820d';
165
+        $properties['duedate'] = 'PT_SYSTIME:PSETID_Appointment:0x820e';
166
+        $properties['flagdueby'] = 'PT_SYSTIME:PSETID_Common:0x8560';
167
+        $properties['commonstart'] = 'PT_SYSTIME:PSETID_Common:0x8516';
168
+        $properties['commonend'] = 'PT_SYSTIME:PSETID_Common:0x8517';
169
+        $properties['recurring'] = 'PT_BOOLEAN:PSETID_Appointment:0x8223';
170
+        $properties['clipstart'] = 'PT_SYSTIME:PSETID_Appointment:0x8235';
171
+        $properties['clipend'] = 'PT_SYSTIME:PSETID_Appointment:0x8236';
172
+        $properties['start_recur_date'] = 'PT_LONG:PSETID_Meeting:0xD';                // StartRecurTime
173
+        $properties['start_recur_time'] = 'PT_LONG:PSETID_Meeting:0xE';                // StartRecurTime
174
+        $properties['end_recur_date'] = 'PT_LONG:PSETID_Meeting:0xF';                // EndRecurDate
175
+        $properties['end_recur_time'] = 'PT_LONG:PSETID_Meeting:0x10';                // EndRecurTime
176
+        $properties['is_exception'] = 'PT_BOOLEAN:PSETID_Meeting:0xA';                // LID_IS_EXCEPTION
177
+        $properties['apptreplyname'] = 'PT_STRING8:PSETID_Appointment:0x8230';
178
+        // Propose new time properties
179
+        $properties['proposed_start_whole'] = 'PT_SYSTIME:PSETID_Appointment:0x8250';
180
+        $properties['proposed_end_whole'] = 'PT_SYSTIME:PSETID_Appointment:0x8251';
181
+        $properties['proposed_duration'] = 'PT_LONG:PSETID_Appointment:0x8256';
182
+        $properties['counter_proposal'] = 'PT_BOOLEAN:PSETID_Appointment:0x8257';
183
+        $properties['recurring_pattern'] = 'PT_STRING8:PSETID_Appointment:0x8232';
184
+        $properties['basedate'] = 'PT_SYSTIME:PSETID_Appointment:0x8228';
185
+        $properties['meetingtype'] = 'PT_LONG:PSETID_Meeting:0x26';
186
+        $properties['timezone_data'] = 'PT_BINARY:PSETID_Appointment:0x8233';
187
+        $properties['timezone'] = 'PT_STRING8:PSETID_Appointment:0x8234';
188
+        $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
189
+        $properties['toattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823B';
190
+        $properties['ccattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823C';
191
+
192
+        $this->proptags = getPropIdsFromStrings($store, $properties);
193
+    }
194
+
195
+    /**
196
+     * Sets the direct booking property. This is an alternative to the setting of the direct booking
197
+     * property through the constructor. However, setting it in the constructor is preferred.
198
+     *
199
+     * @param bool $directBookingSetting
200
+     */
201
+    public function setDirectBooking($directBookingSetting) {
202
+        $this->enableDirectBooking = $directBookingSetting;
203
+    }
204
+
205
+    /**
206
+     * Returns TRUE if the message pointed to is an incoming meeting request and should
207
+     * therefore be replied to with doAccept or doDecline().
208
+     *
209
+     * @param string $messageClass message class to use for checking
210
+     *
211
+     * @return bool returns true if this is a meeting request else false
212
+     */
213
+    public function isMeetingRequest($messageClass = false) {
214
+        if ($messageClass === false) {
215
+            $props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
216
+            $messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
217
+        }
218
+
219
+        if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.request') === 0) {
220
+            return true;
221
+        }
222
+
223
+        return false;
224
+    }
225
+
226
+    /**
227
+     * Returns TRUE if the message pointed to is a returning meeting request response.
228
+     *
229
+     * @param string $messageClass message class to use for checking
230
+     *
231
+     * @return bool returns true if this is a meeting request else false
232
+     */
233
+    public function isMeetingRequestResponse($messageClass = false) {
234
+        if ($messageClass === false) {
235
+            $props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
236
+            $messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
237
+        }
238
+
239
+        if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.resp') === 0) {
240
+            return true;
241
+        }
242
+
243
+        return false;
244
+    }
245
+
246
+    /**
247
+     * Returns TRUE if the message pointed to is a cancellation request.
248
+     *
249
+     * @param 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($messageClass = false) {
254
+        if ($messageClass === false) {
255
+            $props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
256
+            $messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
257
+        }
258
+
259
+        if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.canceled') === 0) {
260
+            return true;
261
+        }
262
+
263
+        return false;
264
+    }
265
+
266
+    /**
267
+     * Function is used to get the last update counter of meeting request.
268
+     *
269
+     * @return bool|Number false when last_updatecounter not found else return last_updatecounter
270
+     */
271
+    public function getLastUpdateCounter() {
272
+        $calendarItemProps = mapi_getprops($this->message, [$this->proptags['last_updatecounter']]);
273
+        if (isset($calendarItemProps) && !empty($calendarItemProps)) {
274
+            return $calendarItemProps[$this->proptags['last_updatecounter']];
275
+        }
276
+
277
+        return false;
278
+    }
279
+
280
+    /**
281
+     * Process an incoming meeting request response. This updates the appointment
282
+     * in your calendar to show whether the user has accepted or declined.
283
+     */
284
+    public function processMeetingRequestResponse() {
285
+        if (!$this->isMeetingRequestResponse()) {
286
+            return;
287
+        }
288
+
289
+        if (!$this->isLocalOrganiser()) {
290
+            return;
291
+        }
292
+
293
+        // Get information we need from the response message
294
+        $messageprops = mapi_getprops($this->message, [
295
+            $this->proptags['goid'],
296
+            $this->proptags['goid2'],
297
+            PR_OWNER_APPT_ID,
298
+            PR_SENT_REPRESENTING_EMAIL_ADDRESS,
299
+            PR_SENT_REPRESENTING_NAME,
300
+            PR_SENT_REPRESENTING_ADDRTYPE,
301
+            PR_SENT_REPRESENTING_ENTRYID,
302
+            PR_SENT_REPRESENTING_SEARCH_KEY,
303
+            PR_MESSAGE_DELIVERY_TIME,
304
+            PR_MESSAGE_CLASS,
305
+            PR_PROCESSED,
306
+            PR_RCVD_REPRESENTING_ENTRYID,
307
+            $this->proptags['proposed_start_whole'],
308
+            $this->proptags['proposed_end_whole'],
309
+            $this->proptags['proposed_duration'],
310
+            $this->proptags['counter_proposal'],
311
+            $this->proptags['attendee_critical_change'],
312
+        ]);
313
+
314
+        $goid2 = $messageprops[$this->proptags['goid2']];
315
+
316
+        if (!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS])) {
317
+            return;
318
+        }
319
+
320
+        // Find basedate in GlobalID(0x3), this can be a response for an occurrence
321
+        $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
322
+
323
+        // check if delegate is processing the response
324
+        if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
325
+            $delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
326
+
327
+            $userStore = $delegatorStore['store'];
328
+            $calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
329
+        }
330
+        else {
331
+            $userStore = $this->store;
332
+            $calFolder = $this->openDefaultCalendar();
333
+        }
334
+
335
+        // check for calendar access
336
+        if ($this->checkCalendarWriteAccess($userStore) !== true) {
337
+            // Throw an exception that we don't have write permissions on calendar folder,
338
+            // allow caller to fill the error message
339
+            throw new MAPIException(null, MAPI_E_NO_ACCESS);
340
+        }
341
+
342
+        $calendarItem = $this->getCorrespondentCalendarItem(true);
343
+
344
+        // Open the calendar items, and update all the recipients of the calendar item that match
345
+        // the email address of the response.
346
+        if ($calendarItem !== false) {
347
+            $this->processResponse($userStore, $calendarItem, $basedate, $messageprops);
348
+        }
349
+    }
350
+
351
+    /**
352
+     * Process every incoming MeetingRequest response.This updates the appointment
353
+     * in your calendar to show whether the user has accepted or declined.
354
+     *
355
+     * @param resource    $store        contains the userStore in which the meeting is created
356
+     * @param MAPIMessage $calendarItem resource of the calendar item for which this response has arrived
357
+     * @param bool        $basedate     if present the create an exception
358
+     * @param array       $messageprops contains message properties
359
+     */
360
+    public function processResponse($store, $calendarItem, $basedate, $messageprops) {
361
+        $senderentryid = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
362
+        $messageclass = $messageprops[PR_MESSAGE_CLASS];
363
+        $deliverytime = $messageprops[PR_MESSAGE_DELIVERY_TIME];
364
+
365
+        // Open the calendar item, find the sender in the recipient table and update all the recipients of the calendar item that match
366
+        // the email address of the response.
367
+        $calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring'], PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $this->proptags['updatecounter']]);
368
+
369
+        // check if meeting response is already processed
370
+        if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
371
+            // meeting is already processed
372
+            return;
373
+        }
374
+        mapi_setprops($this->message, [PR_PROCESSED => true]);
375
+        mapi_savechanges($this->message);
376
+
377
+        // if meeting is updated in organizer's calendar then we don't need to process
378
+        // old response
379
+        if ($this->isMeetingUpdated($basedate)) {
380
+            return;
381
+        }
382
+
383
+        // If basedate is found, then create/modify exception msg and do processing
384
+        if ($basedate && isset($calendarItemProps[$this->proptags['recurring']]) && $calendarItemProps[$this->proptags['recurring']] === true) {
385
+            $recurr = new Recurrence($store, $calendarItem);
386
+
387
+            // Copy properties from meeting request
388
+            $exception_props = mapi_getprops($this->message, [
389
+                PR_OWNER_APPT_ID,
390
+                $this->proptags['proposed_start_whole'],
391
+                $this->proptags['proposed_end_whole'],
392
+                $this->proptags['proposed_duration'],
393
+                $this->proptags['counter_proposal'],
394
+            ]);
395
+
396
+            // Create/modify exception
397
+            if ($recurr->isException($basedate)) {
398
+                $recurr->modifyException($exception_props, $basedate);
399
+            }
400
+            else {
401
+                // When we are creating an exception we need copy recipients from main recurring item
402
+                $recipTable = mapi_message_getrecipienttable($calendarItem);
403
+                $recips = mapi_table_queryallrows($recipTable, $this->recipprops);
404
+
405
+                // Retrieve actual start/due dates from calendar item.
406
+                $exception_props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
407
+                $exception_props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
408
+
409
+                $recurr->createException($exception_props, $basedate, false, $recips);
410
+            }
411
+
412
+            mapi_savechanges($calendarItem);
413
+
414
+            $attach = $recurr->getExceptionAttachment($basedate);
415
+            if ($attach) {
416
+                $recurringItem = $calendarItem;
417
+                $calendarItem = mapi_attach_openobj($attach, MAPI_MODIFY);
418
+            }
419
+            else {
420
+                return false;
421
+            }
422
+        }
423
+
424
+        // Get the recipients of the calendar item
425
+        $reciptable = mapi_message_getrecipienttable($calendarItem);
426
+        $recipients = mapi_table_queryallrows($reciptable, $this->recipprops);
427
+
428
+        // FIXME we should look at the updatecounter property and compare it
429
+        // to the counter in the recipient to see if this update is actually
430
+        // newer than the status in the calendar item
431
+        $found = false;
432
+
433
+        $totalrecips = 0;
434
+        $acceptedrecips = 0;
435
+        foreach ($recipients as $recipient) {
436
+            ++$totalrecips;
437
+            if (isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID], $senderentryid)) {
438
+                $found = true;
439
+
440
+                /*
441 441
 				 * If value of attendee_critical_change on meeting response mail is less than PR_RECIPIENT_TRACKSTATUS_TIME
442 442
 				 * on the corresponding recipientRow of meeting then we ignore this response mail.
443 443
 				 */
444
-				if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME])) {
445
-					continue;
446
-				}
447
-
448
-				// The email address matches, update the row
449
-				$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
450
-				$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']];
451
-
452
-				// If this is a counter proposal, set the proposal properties in the recipient row
453
-				if (isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]) {
454
-					$recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']];
455
-					$recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']];
456
-					$recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']];
457
-				}
458
-
459
-				mapi_message_modifyrecipients($calendarItem, MODRECIP_MODIFY, [$recipient]);
460
-			}
461
-			if (isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
462
-				++$acceptedrecips;
463
-			}
464
-		}
465
-
466
-		// If the recipient was not found in the original calendar item,
467
-		// then add the recpient as a new optional recipient
468
-		if (!$found) {
469
-			$recipient = [];
470
-			$recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
471
-			$recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
472
-			$recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
473
-			$recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
474
-			$recipient[PR_RECIPIENT_TYPE] = MAPI_CC;
475
-			$recipient[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
476
-			$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
477
-			$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime;
478
-
479
-			// If this is a counter proposal, set the proposal properties in the recipient row
480
-			if (isset($messageprops[$this->proptags['counter_proposal']])) {
481
-				$recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']];
482
-				$recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']];
483
-				$recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']];
484
-			}
485
-
486
-			mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
487
-			++$totalrecips;
488
-			if ($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
489
-				++$acceptedrecips;
490
-			}
491
-		}
492
-
493
-		// TODO: Update counter proposal number property on message
494
-		/*
444
+                if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME])) {
445
+                    continue;
446
+                }
447
+
448
+                // The email address matches, update the row
449
+                $recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
450
+                $recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']];
451
+
452
+                // If this is a counter proposal, set the proposal properties in the recipient row
453
+                if (isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]) {
454
+                    $recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']];
455
+                    $recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']];
456
+                    $recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']];
457
+                }
458
+
459
+                mapi_message_modifyrecipients($calendarItem, MODRECIP_MODIFY, [$recipient]);
460
+            }
461
+            if (isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
462
+                ++$acceptedrecips;
463
+            }
464
+        }
465
+
466
+        // If the recipient was not found in the original calendar item,
467
+        // then add the recpient as a new optional recipient
468
+        if (!$found) {
469
+            $recipient = [];
470
+            $recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
471
+            $recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
472
+            $recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
473
+            $recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
474
+            $recipient[PR_RECIPIENT_TYPE] = MAPI_CC;
475
+            $recipient[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
476
+            $recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
477
+            $recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime;
478
+
479
+            // If this is a counter proposal, set the proposal properties in the recipient row
480
+            if (isset($messageprops[$this->proptags['counter_proposal']])) {
481
+                $recipient[PR_PROPOSENEWTIME_START] = $messageprops[$this->proptags['proposed_start_whole']];
482
+                $recipient[PR_PROPOSENEWTIME_END] = $messageprops[$this->proptags['proposed_end_whole']];
483
+                $recipient[PR_PROPOSEDNEWTIME] = $messageprops[$this->proptags['counter_proposal']];
484
+            }
485
+
486
+            mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
487
+            ++$totalrecips;
488
+            if ($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
489
+                ++$acceptedrecips;
490
+            }
491
+        }
492
+
493
+        // TODO: Update counter proposal number property on message
494
+        /*
495 495
 		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.
496 496
 		*/
497
-		// If this is a counter proposal, set the counter proposal indicator boolean
498
-		if (isset($messageprops[$this->proptags['counter_proposal']])) {
499
-			$props = [];
500
-			if ($messageprops[$this->proptags['counter_proposal']]) {
501
-				$props[$this->proptags['counter_proposal']] = true;
502
-			}
503
-			else {
504
-				$props[$this->proptags['counter_proposal']] = false;
505
-			}
506
-
507
-			mapi_setprops($calendarItem, $props);
508
-		}
509
-
510
-		mapi_savechanges($calendarItem);
511
-		if (isset($attach)) {
512
-			mapi_savechanges($attach);
513
-			mapi_savechanges($recurringItem);
514
-		}
515
-	}
516
-
517
-	/**
518
-	 * Process an incoming meeting request cancellation. This updates the
519
-	 * appointment in your calendar to show that the meeting has been cancelled.
520
-	 */
521
-	public function processMeetingCancellation() {
522
-		if (!$this->isMeetingCancellation()) {
523
-			return;
524
-		}
525
-
526
-		if ($this->isLocalOrganiser()) {
527
-			return;
528
-		}
529
-
530
-		if (!$this->isInCalendar()) {
531
-			return;
532
-		}
533
-
534
-		$listProperties = $this->proptags;
535
-		$listProperties['subject'] = PR_SUBJECT;
536
-		$listProperties['sent_representing_name'] = PR_SENT_REPRESENTING_NAME;
537
-		$listProperties['sent_representing_address_type'] = PR_SENT_REPRESENTING_ADDRTYPE;
538
-		$listProperties['sent_representing_email_address'] = PR_SENT_REPRESENTING_EMAIL_ADDRESS;
539
-		$listProperties['sent_representing_entryid'] = PR_SENT_REPRESENTING_ENTRYID;
540
-		$listProperties['sent_representing_search_key'] = PR_SENT_REPRESENTING_SEARCH_KEY;
541
-		$listProperties['rcvd_representing_name'] = PR_RCVD_REPRESENTING_NAME;
542
-		$listProperties['rcvd_representing_address_type'] = PR_RCVD_REPRESENTING_ADDRTYPE;
543
-		$listProperties['rcvd_representing_email_address'] = PR_RCVD_REPRESENTING_EMAIL_ADDRESS;
544
-		$listProperties['rcvd_representing_entryid'] = PR_RCVD_REPRESENTING_ENTRYID;
545
-		$listProperties['rcvd_representing_search_key'] = PR_RCVD_REPRESENTING_SEARCH_KEY;
546
-		$messageProps = mapi_getprops($this->message, $listProperties);
547
-
548
-		$goid = $messageProps[$this->proptags['goid']];    // GlobalID (0x3)
549
-		if (!isset($goid)) {
550
-			return;
551
-		}
552
-
553
-		// get delegator store, if delegate is processing this cancellation
554
-		if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
555
-			$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
556
-
557
-			$store = $delegatorStore['store'];
558
-			$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
559
-		}
560
-		else {
561
-			$store = $this->store;
562
-			$calFolder = $this->openDefaultCalendar();
563
-		}
564
-
565
-		// check for calendar access
566
-		if ($this->checkCalendarWriteAccess($store) !== true) {
567
-			// Throw an exception that we don't have write permissions on calendar folder,
568
-			// allow caller to fill the error message
569
-			throw new MAPIException(null, MAPI_E_NO_ACCESS);
570
-		}
571
-
572
-		$calendarItem = $this->getCorrespondentCalendarItem(true);
573
-		$basedate = $this->getBasedateFromGlobalID($goid);
574
-
575
-		if ($calendarItem !== false) {
576
-			// if basedate is provided and we could not find the item then it could be that we are processing
577
-			// an exception so get the exception and process it
578
-			if ($basedate) {
579
-				$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring']]);
580
-				if ($calendarItemProps[$this->proptags['recurring']] === true) {
581
-					$recurr = new Recurrence($store, $calendarItem);
582
-
583
-					// Set message class
584
-					$messageProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
585
-
586
-					if ($recurr->isException($basedate)) {
587
-						$recurr->modifyException($messageProps, $basedate);
588
-					}
589
-					else {
590
-						$recurr->createException($messageProps, $basedate);
591
-					}
592
-				}
593
-			}
594
-			else {
595
-				// set the properties of the cancellation object
596
-				mapi_setprops($calendarItem, $messageProps);
597
-			}
598
-
599
-			mapi_savechanges($calendarItem);
600
-		}
601
-	}
602
-
603
-	/**
604
-	 * Returns true if the corresponding calendar items exists in the celendar folder for this
605
-	 * meeting request/response/cancellation.
606
-	 */
607
-	public function isInCalendar() {
608
-		// @TODO check for deleted exceptions
609
-		return $this->getCorrespondentCalendarItem(false) !== false;
610
-	}
611
-
612
-	/**
613
-	 * Accepts the meeting request by moving the item to the calendar
614
-	 * and sending a confirmation message back to the sender. If $tentative
615
-	 * is TRUE, then the item is accepted tentatively. After accepting, you
616
-	 * can't use this class instance any more. The message is closed. If you
617
-	 * specify TRUE for 'move', then the item is actually moved (from your
618
-	 * inbox probably) to the calendar. If you don't, it is copied into
619
-	 * your calendar.
620
-	 *
621
-	 * @param bool   $tentative            true if user as tentative accepted the meeting
622
-	 * @param bool   $sendresponse         true if a response has to be send to organizer
623
-	 * @param bool   $move                 true if the meeting request should be moved to the deleted items after processing
624
-	 * @param string $newProposedStartTime contains starttime if user has proposed other time
625
-	 * @param string $newProposedEndTime   contains endtime if user has proposed other time
626
-	 * @param string $basedate             start of day of occurrence for which user has accepted the recurrent meeting
627
-	 * @param bool   $isImported           true to indicate that MR is imported from .ics or .vcs file else it false.
628
-	 * @param mixed  $body
629
-	 * @param mixed  $userAction
630
-	 * @param mixed  $store
631
-	 *
632
-	 * @return string $entryid entryid of item which created/updated in calendar
633
-	 */
634
-	public function doAccept($tentative, $sendresponse, $move, $newProposedStartTime = false, $newProposedEndTime = false, $body = false, $userAction = false, $store = false, $basedate = false, $isImported = false) {
635
-		if ($this->isLocalOrganiser()) {
636
-			return false;
637
-		}
638
-
639
-		// Remove any previous calendar items with this goid and appt id
640
-		$messageprops = mapi_getprops($this->message, [PR_ENTRYID, PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['updatecounter'], PR_PROCESSED, PR_RCVD_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID]);
641
-
642
-		// If this meeting request is received by a delegate then open delegator's store.
643
-		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
644
-			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
645
-
646
-			$store = $delegatorStore['store'];
647
-			$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
648
-		}
649
-		else {
650
-			$calFolder = $this->openDefaultCalendar();
651
-			$store = $this->store;
652
-		}
653
-
654
-		// check for calendar access
655
-		if ($this->checkCalendarWriteAccess($store) !== true) {
656
-			// Throw an exception that we don't have write permissions on calendar folder,
657
-			// allow caller to fill the error message
658
-			throw new MAPIException(null, MAPI_E_NO_ACCESS);
659
-		}
660
-
661
-		// if meeting is out dated then don't process it
662
-		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $this->isMeetingOutOfDate()) {
663
-			return false;
664
-		}
665
-
666
-		/*
497
+        // If this is a counter proposal, set the counter proposal indicator boolean
498
+        if (isset($messageprops[$this->proptags['counter_proposal']])) {
499
+            $props = [];
500
+            if ($messageprops[$this->proptags['counter_proposal']]) {
501
+                $props[$this->proptags['counter_proposal']] = true;
502
+            }
503
+            else {
504
+                $props[$this->proptags['counter_proposal']] = false;
505
+            }
506
+
507
+            mapi_setprops($calendarItem, $props);
508
+        }
509
+
510
+        mapi_savechanges($calendarItem);
511
+        if (isset($attach)) {
512
+            mapi_savechanges($attach);
513
+            mapi_savechanges($recurringItem);
514
+        }
515
+    }
516
+
517
+    /**
518
+     * Process an incoming meeting request cancellation. This updates the
519
+     * appointment in your calendar to show that the meeting has been cancelled.
520
+     */
521
+    public function processMeetingCancellation() {
522
+        if (!$this->isMeetingCancellation()) {
523
+            return;
524
+        }
525
+
526
+        if ($this->isLocalOrganiser()) {
527
+            return;
528
+        }
529
+
530
+        if (!$this->isInCalendar()) {
531
+            return;
532
+        }
533
+
534
+        $listProperties = $this->proptags;
535
+        $listProperties['subject'] = PR_SUBJECT;
536
+        $listProperties['sent_representing_name'] = PR_SENT_REPRESENTING_NAME;
537
+        $listProperties['sent_representing_address_type'] = PR_SENT_REPRESENTING_ADDRTYPE;
538
+        $listProperties['sent_representing_email_address'] = PR_SENT_REPRESENTING_EMAIL_ADDRESS;
539
+        $listProperties['sent_representing_entryid'] = PR_SENT_REPRESENTING_ENTRYID;
540
+        $listProperties['sent_representing_search_key'] = PR_SENT_REPRESENTING_SEARCH_KEY;
541
+        $listProperties['rcvd_representing_name'] = PR_RCVD_REPRESENTING_NAME;
542
+        $listProperties['rcvd_representing_address_type'] = PR_RCVD_REPRESENTING_ADDRTYPE;
543
+        $listProperties['rcvd_representing_email_address'] = PR_RCVD_REPRESENTING_EMAIL_ADDRESS;
544
+        $listProperties['rcvd_representing_entryid'] = PR_RCVD_REPRESENTING_ENTRYID;
545
+        $listProperties['rcvd_representing_search_key'] = PR_RCVD_REPRESENTING_SEARCH_KEY;
546
+        $messageProps = mapi_getprops($this->message, $listProperties);
547
+
548
+        $goid = $messageProps[$this->proptags['goid']];    // GlobalID (0x3)
549
+        if (!isset($goid)) {
550
+            return;
551
+        }
552
+
553
+        // get delegator store, if delegate is processing this cancellation
554
+        if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
555
+            $delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
556
+
557
+            $store = $delegatorStore['store'];
558
+            $calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
559
+        }
560
+        else {
561
+            $store = $this->store;
562
+            $calFolder = $this->openDefaultCalendar();
563
+        }
564
+
565
+        // check for calendar access
566
+        if ($this->checkCalendarWriteAccess($store) !== true) {
567
+            // Throw an exception that we don't have write permissions on calendar folder,
568
+            // allow caller to fill the error message
569
+            throw new MAPIException(null, MAPI_E_NO_ACCESS);
570
+        }
571
+
572
+        $calendarItem = $this->getCorrespondentCalendarItem(true);
573
+        $basedate = $this->getBasedateFromGlobalID($goid);
574
+
575
+        if ($calendarItem !== false) {
576
+            // if basedate is provided and we could not find the item then it could be that we are processing
577
+            // an exception so get the exception and process it
578
+            if ($basedate) {
579
+                $calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring']]);
580
+                if ($calendarItemProps[$this->proptags['recurring']] === true) {
581
+                    $recurr = new Recurrence($store, $calendarItem);
582
+
583
+                    // Set message class
584
+                    $messageProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
585
+
586
+                    if ($recurr->isException($basedate)) {
587
+                        $recurr->modifyException($messageProps, $basedate);
588
+                    }
589
+                    else {
590
+                        $recurr->createException($messageProps, $basedate);
591
+                    }
592
+                }
593
+            }
594
+            else {
595
+                // set the properties of the cancellation object
596
+                mapi_setprops($calendarItem, $messageProps);
597
+            }
598
+
599
+            mapi_savechanges($calendarItem);
600
+        }
601
+    }
602
+
603
+    /**
604
+     * Returns true if the corresponding calendar items exists in the celendar folder for this
605
+     * meeting request/response/cancellation.
606
+     */
607
+    public function isInCalendar() {
608
+        // @TODO check for deleted exceptions
609
+        return $this->getCorrespondentCalendarItem(false) !== false;
610
+    }
611
+
612
+    /**
613
+     * Accepts the meeting request by moving the item to the calendar
614
+     * and sending a confirmation message back to the sender. If $tentative
615
+     * is TRUE, then the item is accepted tentatively. After accepting, you
616
+     * can't use this class instance any more. The message is closed. If you
617
+     * specify TRUE for 'move', then the item is actually moved (from your
618
+     * inbox probably) to the calendar. If you don't, it is copied into
619
+     * your calendar.
620
+     *
621
+     * @param bool   $tentative            true if user as tentative accepted the meeting
622
+     * @param bool   $sendresponse         true if a response has to be send to organizer
623
+     * @param bool   $move                 true if the meeting request should be moved to the deleted items after processing
624
+     * @param string $newProposedStartTime contains starttime if user has proposed other time
625
+     * @param string $newProposedEndTime   contains endtime if user has proposed other time
626
+     * @param string $basedate             start of day of occurrence for which user has accepted the recurrent meeting
627
+     * @param bool   $isImported           true to indicate that MR is imported from .ics or .vcs file else it false.
628
+     * @param mixed  $body
629
+     * @param mixed  $userAction
630
+     * @param mixed  $store
631
+     *
632
+     * @return string $entryid entryid of item which created/updated in calendar
633
+     */
634
+    public function doAccept($tentative, $sendresponse, $move, $newProposedStartTime = false, $newProposedEndTime = false, $body = false, $userAction = false, $store = false, $basedate = false, $isImported = false) {
635
+        if ($this->isLocalOrganiser()) {
636
+            return false;
637
+        }
638
+
639
+        // Remove any previous calendar items with this goid and appt id
640
+        $messageprops = mapi_getprops($this->message, [PR_ENTRYID, PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['updatecounter'], PR_PROCESSED, PR_RCVD_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID]);
641
+
642
+        // If this meeting request is received by a delegate then open delegator's store.
643
+        if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
644
+            $delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
645
+
646
+            $store = $delegatorStore['store'];
647
+            $calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
648
+        }
649
+        else {
650
+            $calFolder = $this->openDefaultCalendar();
651
+            $store = $this->store;
652
+        }
653
+
654
+        // check for calendar access
655
+        if ($this->checkCalendarWriteAccess($store) !== true) {
656
+            // Throw an exception that we don't have write permissions on calendar folder,
657
+            // allow caller to fill the error message
658
+            throw new MAPIException(null, MAPI_E_NO_ACCESS);
659
+        }
660
+
661
+        // if meeting is out dated then don't process it
662
+        if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $this->isMeetingOutOfDate()) {
663
+            return false;
664
+        }
665
+
666
+        /*
667 667
 		 *    if this function is called automatically with meeting request object then there will be
668 668
 		 *    two possibilitites
669 669
 		 *    1) meeting request is opened first time, in this case make a tentative appointment in
670 670
 		 * recipient's calendar
671 671
 		 *    2) after this every subsequent request to open meeting request will not do any processing
672 672
 		 */
673
-		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction == false) {
674
-			if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
675
-				// if meeting request is already processed then don't do anything
676
-				return false;
677
-			}
678
-
679
-			// if correspondent calendar item is already processed then don't do anything
680
-			$calendarItem = $this->getCorrespondentCalendarItem();
681
-			$calendarItemProps = mapi_getprops($calendarItem, [PR_PROCESSED]);
682
-			if (isset($calendarItemProps[PR_PROCESSED]) && $calendarItemProps[PR_PROCESSED] == true) {
683
-				// mark meeting-request mail as processed as well
684
-				mapi_setprops($this->message, [PR_PROCESSED => true]);
685
-				mapi_savechanges($this->message);
686
-
687
-				return false;
688
-			}
689
-		}
690
-
691
-		// Retrieve basedate from globalID, if it is not received as argument
692
-		if (!$basedate) {
693
-			$basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
694
-		}
695
-
696
-		// set counter proposal properties in calendar item when proposing new time
697
-		$proposeNewTimeProps = [];
698
-		if ($newProposedStartTime && $newProposedEndTime) {
699
-			$proposeNewTimeProps[$this->proptags['proposed_start_whole']] = $newProposedStartTime;
700
-			$proposeNewTimeProps[$this->proptags['proposed_end_whole']] = $newProposedEndTime;
701
-			$proposeNewTimeProps[$this->proptags['proposed_duration']] = round($newProposedEndTime - $newProposedStartTime) / 60;
702
-			$proposeNewTimeProps[$this->proptags['counter_proposal']] = true;
703
-		}
704
-
705
-		// While sender is receiver then we have to process the meeting request as per the intended busy status
706
-		// instead of tentative, and accept the same as per the intended busystatus.
707
-		$senderEntryId = isset($messageprops[PR_SENT_REPRESENTING_ENTRYID]) ? $messageprops[PR_SENT_REPRESENTING_ENTRYID] : $messageprops[PR_SENDER_ENTRYID];
708
-		if (isset($messageprops[PR_RECEIVED_BY_ENTRYID]) && MAPIUtils::CompareEntryIds($senderEntryId, $messageprops[PR_RECEIVED_BY_ENTRYID])) {
709
-			$entryid = $this->accept(false, $sendresponse, $move, $proposeNewTimeProps, $body, true, $store, $calFolder, $basedate);
710
-		}
711
-		else {
712
-			$entryid = $this->accept($tentative, $sendresponse, $move, $proposeNewTimeProps, $body, $userAction, $store, $calFolder, $basedate);
713
-		}
714
-
715
-		// if we have first time processed this meeting then set PR_PROCESSED property
716
-		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction === false && $isImported === false) {
717
-			if (!isset($messageprops[PR_PROCESSED]) || $messageprops[PR_PROCESSED] != true) {
718
-				// set processed flag
719
-				mapi_setprops($this->message, [PR_PROCESSED => true]);
720
-				mapi_savechanges($this->message);
721
-			}
722
-		}
723
-
724
-		return $entryid;
725
-	}
726
-
727
-	public function accept($tentative, $sendresponse, $move, $proposeNewTimeProps = [], $body = false, $userAction = false, $store, $calFolder, $basedate = false) {
728
-		$messageprops = mapi_getprops($this->message);
729
-		$isDelegate = isset($messageprops[PR_RCVD_REPRESENTING_NAME]);
730
-
731
-		if ($sendresponse) {
732
-			$this->createResponse($tentative ? olResponseTentative : olResponseAccepted, $proposeNewTimeProps, $body, $store, $basedate, $calFolder);
733
-		}
734
-
735
-		/*
673
+        if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction == false) {
674
+            if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
675
+                // if meeting request is already processed then don't do anything
676
+                return false;
677
+            }
678
+
679
+            // if correspondent calendar item is already processed then don't do anything
680
+            $calendarItem = $this->getCorrespondentCalendarItem();
681
+            $calendarItemProps = mapi_getprops($calendarItem, [PR_PROCESSED]);
682
+            if (isset($calendarItemProps[PR_PROCESSED]) && $calendarItemProps[PR_PROCESSED] == true) {
683
+                // mark meeting-request mail as processed as well
684
+                mapi_setprops($this->message, [PR_PROCESSED => true]);
685
+                mapi_savechanges($this->message);
686
+
687
+                return false;
688
+            }
689
+        }
690
+
691
+        // Retrieve basedate from globalID, if it is not received as argument
692
+        if (!$basedate) {
693
+            $basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
694
+        }
695
+
696
+        // set counter proposal properties in calendar item when proposing new time
697
+        $proposeNewTimeProps = [];
698
+        if ($newProposedStartTime && $newProposedEndTime) {
699
+            $proposeNewTimeProps[$this->proptags['proposed_start_whole']] = $newProposedStartTime;
700
+            $proposeNewTimeProps[$this->proptags['proposed_end_whole']] = $newProposedEndTime;
701
+            $proposeNewTimeProps[$this->proptags['proposed_duration']] = round($newProposedEndTime - $newProposedStartTime) / 60;
702
+            $proposeNewTimeProps[$this->proptags['counter_proposal']] = true;
703
+        }
704
+
705
+        // While sender is receiver then we have to process the meeting request as per the intended busy status
706
+        // instead of tentative, and accept the same as per the intended busystatus.
707
+        $senderEntryId = isset($messageprops[PR_SENT_REPRESENTING_ENTRYID]) ? $messageprops[PR_SENT_REPRESENTING_ENTRYID] : $messageprops[PR_SENDER_ENTRYID];
708
+        if (isset($messageprops[PR_RECEIVED_BY_ENTRYID]) && MAPIUtils::CompareEntryIds($senderEntryId, $messageprops[PR_RECEIVED_BY_ENTRYID])) {
709
+            $entryid = $this->accept(false, $sendresponse, $move, $proposeNewTimeProps, $body, true, $store, $calFolder, $basedate);
710
+        }
711
+        else {
712
+            $entryid = $this->accept($tentative, $sendresponse, $move, $proposeNewTimeProps, $body, $userAction, $store, $calFolder, $basedate);
713
+        }
714
+
715
+        // if we have first time processed this meeting then set PR_PROCESSED property
716
+        if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction === false && $isImported === false) {
717
+            if (!isset($messageprops[PR_PROCESSED]) || $messageprops[PR_PROCESSED] != true) {
718
+                // set processed flag
719
+                mapi_setprops($this->message, [PR_PROCESSED => true]);
720
+                mapi_savechanges($this->message);
721
+            }
722
+        }
723
+
724
+        return $entryid;
725
+    }
726
+
727
+    public function accept($tentative, $sendresponse, $move, $proposeNewTimeProps = [], $body = false, $userAction = false, $store, $calFolder, $basedate = false) {
728
+        $messageprops = mapi_getprops($this->message);
729
+        $isDelegate = isset($messageprops[PR_RCVD_REPRESENTING_NAME]);
730
+
731
+        if ($sendresponse) {
732
+            $this->createResponse($tentative ? olResponseTentative : olResponseAccepted, $proposeNewTimeProps, $body, $store, $basedate, $calFolder);
733
+        }
734
+
735
+        /*
736 736
 		 * Further processing depends on what user is receiving. User can receive recurring item, a single occurrence or a normal meeting.
737 737
 		 * 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.
738 738
 		 * 2) If single occurrence then find occurrence itself using globalID and if item is not found then use cleanGlobalID to find main recurring item
@@ -742,2394 +742,2394 @@  discard block
 block discarded – undo
742 742
 		 * 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.
743 743
 		 * If user is responding from calendar then item is opened and properties are set such as meetingstatus, responsestatus, busystatus etc.
744 744
 		 */
745
-		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
746
-			// While processing the item mark it as read.
747
-			mapi_message_setreadflag($this->message, SUPPRESS_RECEIPT);
748
-
749
-			// This meeting request item is recurring, so find all occurrences and saves them all as exceptions to this meeting request item.
750
-			if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] == true) {
751
-				$calendarItem = false;
752
-
753
-				// Find main recurring item based on GlobalID (0x3)
754
-				$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
755
-				if (is_array($items)) {
756
-					foreach ($items as $key => $entryid) {
757
-						$calendarItem = mapi_msgstore_openentry($store, $entryid);
758
-					}
759
-				}
760
-
761
-				$processed = false;
762
-				if (!$calendarItem) {
763
-					// Recurring item not found, so create new meeting in Calendar
764
-					$calendarItem = mapi_folder_createmessage($calFolder);
765
-				}
766
-				else {
767
-					// we have found the main recurring item, check if this meeting request is already processed
768
-					if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
769
-						// only set required properties, other properties are already copied when processing this meeting request
770
-						// for the first time
771
-						$processed = true;
772
-					}
773
-				}
774
-
775
-				if (!$processed) {
776
-					// get all the properties and copy that to calendar item
777
-					$props = mapi_getprops($this->message);
778
-
779
-					/*
745
+        if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
746
+            // While processing the item mark it as read.
747
+            mapi_message_setreadflag($this->message, SUPPRESS_RECEIPT);
748
+
749
+            // This meeting request item is recurring, so find all occurrences and saves them all as exceptions to this meeting request item.
750
+            if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] == true) {
751
+                $calendarItem = false;
752
+
753
+                // Find main recurring item based on GlobalID (0x3)
754
+                $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
755
+                if (is_array($items)) {
756
+                    foreach ($items as $key => $entryid) {
757
+                        $calendarItem = mapi_msgstore_openentry($store, $entryid);
758
+                    }
759
+                }
760
+
761
+                $processed = false;
762
+                if (!$calendarItem) {
763
+                    // Recurring item not found, so create new meeting in Calendar
764
+                    $calendarItem = mapi_folder_createmessage($calFolder);
765
+                }
766
+                else {
767
+                    // we have found the main recurring item, check if this meeting request is already processed
768
+                    if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
769
+                        // only set required properties, other properties are already copied when processing this meeting request
770
+                        // for the first time
771
+                        $processed = true;
772
+                    }
773
+                }
774
+
775
+                if (!$processed) {
776
+                    // get all the properties and copy that to calendar item
777
+                    $props = mapi_getprops($this->message);
778
+
779
+                    /*
780 780
 					 * the client which has sent this meeting request can generate wrong flagdueby
781 781
 					 * time (mainly OL), so regenerate that property so we will always show reminder
782 782
 					 * on right time
783 783
 					 */
784
-					if (isset($props[$this->proptags['reminderminutes']])) {
785
-						$props[$this->proptags['flagdueby']] = $props[$this->proptags['startdate']] - ($props[$this->proptags['reminderminutes']] * 60);
786
-					}
787
-				}
788
-				else {
789
-					// only get required properties so we will not overwrite existing updated properties from calendar
790
-					$props = mapi_getprops($this->message, [PR_ENTRYID]);
791
-				}
792
-
793
-				// While we applying updates of MR then all local categories will be removed,
794
-				// So get the local categories of all occurrence before applying update from organiser.
795
-				$localCategories = $this->getLocalCategories($calendarItem, $store, $calFolder);
796
-
797
-				$props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
798
-				// When meeting requests are generated by third-party solutions, we might be missing the updatecounter property.
799
-				if (!isset($props[$this->proptags['updatecounter']])) {
800
-					$props[$this->proptags['updatecounter']] = 0;
801
-				}
802
-				$props[$this->proptags['meetingstatus']] = olMeetingReceived;
803
-				// when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
804
-				$props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
805
-
806
-				if (isset($props[$this->proptags['intendedbusystatus']])) {
807
-					if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) {
808
-						$props[$this->proptags['busystatus']] = fbTentative;
809
-					}
810
-					else {
811
-						$props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
812
-					}
813
-					// we already have intendedbusystatus value in $props so no need to copy it
814
-				}
815
-				else {
816
-					$props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
817
-				}
818
-
819
-				if ($userAction) {
820
-					$addrInfo = $this->getOwnerAddress($this->store);
821
-
822
-					// if user has responded then set replytime and name
823
-					$props[$this->proptags['replytime']] = time();
824
-					if (!empty($addrInfo)) {
825
-						// @FIXME conditionally set this property only for delegation case
826
-						$props[$this->proptags['apptreplyname']] = $addrInfo[0];
827
-					}
828
-				}
829
-
830
-				mapi_setprops($calendarItem, $props);
831
-
832
-				// we have already processed attachments and recipients, so no need to do it again
833
-				if (!$processed) {
834
-					// Copy attachments too
835
-					$this->replaceAttachments($this->message, $calendarItem);
836
-					// Copy recipients too
837
-					$this->replaceRecipients($this->message, $calendarItem, $isDelegate);
838
-				}
839
-
840
-				// Find all occurrences based on CleanGlobalID (0x23)
841
-				// there will be no exceptions left if $processed is true, but even if it doesn't hurt to recheck
842
-				$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
843
-				if (is_array($items)) {
844
-					// Save all existing occurrence as exceptions
845
-					foreach ($items as $entryid) {
846
-						// Open occurrence
847
-						$occurrenceItem = mapi_msgstore_openentry($store, $entryid);
848
-
849
-						// Save occurrence into main recurring item as exception
850
-						if ($occurrenceItem) {
851
-							$occurrenceItemProps = mapi_getprops($occurrenceItem, [$this->proptags['goid'], $this->proptags['recurring']]);
852
-
853
-							// Find basedate of occurrence item
854
-							$basedate = $this->getBasedateFromGlobalID($occurrenceItemProps[$this->proptags['goid']]);
855
-							if ($basedate && $occurrenceItemProps[$this->proptags['recurring']] != true) {
856
-								$this->mergeException($calendarItem, $occurrenceItem, $basedate, $store);
857
-							}
858
-						}
859
-					}
860
-				}
861
-
862
-				mapi_savechanges($calendarItem);
863
-
864
-				// After applying update of organiser all local categories of occurrence was removed,
865
-				// So if local categories exist then apply it on respective occurrence.
866
-				if (!empty($localCategories)) {
867
-					$this->applyLocalCategories($calendarItem, $store, $localCategories);
868
-				}
869
-
870
-				if ($move) {
871
-					// open wastebasket of currently logged in user and move the meeting request to it
872
-					// for delegates this will be delegate's wastebasket folder
873
-					$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
874
-					mapi_folder_copymessages($calFolder, [$props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
875
-				}
876
-
877
-				$entryid = $props[PR_ENTRYID];
878
-			}
879
-			else {
880
-				/**
881
-				 * This meeting request is not recurring, so can be an exception or normal meeting.
882
-				 * If exception then find main recurring item and update exception
883
-				 * If main recurring item is not found then put exception into Calendar as normal meeting.
884
-				 */
885
-				$calendarItem = false;
886
-
887
-				// We found basedate in GlobalID of this meeting request, so this meeting request if for an occurrence.
888
-				if ($basedate) {
889
-					// Find main recurring item from CleanGlobalID of this meeting request
890
-					$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
891
-					if (is_array($items)) {
892
-						foreach ($items as $key => $entryid) {
893
-							$calendarItem = mapi_msgstore_openentry($store, $entryid);
894
-						}
895
-					}
896
-
897
-					// Main recurring item is found, so now update exception
898
-					if ($calendarItem) {
899
-						$this->acceptException($calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate);
900
-						$calendarItemProps = mapi_getprops($calendarItem, [PR_ENTRYID]);
901
-						$entryid = $calendarItemProps[PR_ENTRYID];
902
-					}
903
-				}
904
-
905
-				if (!$calendarItem) {
906
-					$items = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
907
-					if (is_array($items)) {
908
-						// Get local categories before deleting MR.
909
-						$message = mapi_msgstore_openentry($store, $items[0]);
910
-						$localCategories = mapi_getprops($message, [$this->proptags['categories']]);
911
-						mapi_folder_deletemessages($calFolder, $items);
912
-					}
913
-
914
-					if ($move) {
915
-						// All we have to do is open the default calendar,
916
-						// set the message class correctly to be an appointment item
917
-						// and move it to the calendar folder
918
-						$sourcefolder = $this->openParentFolder();
919
-
920
-						// create a new calendar message, and copy the message to there,
921
-						// since we want to delete (move to wastebasket) the original message
922
-						$old_entryid = mapi_getprops($this->message, [PR_ENTRYID]);
923
-						$calmsg = mapi_folder_createmessage($calFolder);
924
-						mapi_copyto($this->message, [], [], $calmsg); /* includes attachments and recipients */
925
-
926
-						// After creating new MR, If local categories exist then apply it on new MR.
927
-						if (!empty($localCategories)) {
928
-							mapi_setprops($calmsg, $localCategories);
929
-						}
930
-
931
-						$calItemProps = [];
932
-						$calItemProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
933
-
934
-						/*
784
+                    if (isset($props[$this->proptags['reminderminutes']])) {
785
+                        $props[$this->proptags['flagdueby']] = $props[$this->proptags['startdate']] - ($props[$this->proptags['reminderminutes']] * 60);
786
+                    }
787
+                }
788
+                else {
789
+                    // only get required properties so we will not overwrite existing updated properties from calendar
790
+                    $props = mapi_getprops($this->message, [PR_ENTRYID]);
791
+                }
792
+
793
+                // While we applying updates of MR then all local categories will be removed,
794
+                // So get the local categories of all occurrence before applying update from organiser.
795
+                $localCategories = $this->getLocalCategories($calendarItem, $store, $calFolder);
796
+
797
+                $props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
798
+                // When meeting requests are generated by third-party solutions, we might be missing the updatecounter property.
799
+                if (!isset($props[$this->proptags['updatecounter']])) {
800
+                    $props[$this->proptags['updatecounter']] = 0;
801
+                }
802
+                $props[$this->proptags['meetingstatus']] = olMeetingReceived;
803
+                // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
804
+                $props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
805
+
806
+                if (isset($props[$this->proptags['intendedbusystatus']])) {
807
+                    if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) {
808
+                        $props[$this->proptags['busystatus']] = fbTentative;
809
+                    }
810
+                    else {
811
+                        $props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
812
+                    }
813
+                    // we already have intendedbusystatus value in $props so no need to copy it
814
+                }
815
+                else {
816
+                    $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
817
+                }
818
+
819
+                if ($userAction) {
820
+                    $addrInfo = $this->getOwnerAddress($this->store);
821
+
822
+                    // if user has responded then set replytime and name
823
+                    $props[$this->proptags['replytime']] = time();
824
+                    if (!empty($addrInfo)) {
825
+                        // @FIXME conditionally set this property only for delegation case
826
+                        $props[$this->proptags['apptreplyname']] = $addrInfo[0];
827
+                    }
828
+                }
829
+
830
+                mapi_setprops($calendarItem, $props);
831
+
832
+                // we have already processed attachments and recipients, so no need to do it again
833
+                if (!$processed) {
834
+                    // Copy attachments too
835
+                    $this->replaceAttachments($this->message, $calendarItem);
836
+                    // Copy recipients too
837
+                    $this->replaceRecipients($this->message, $calendarItem, $isDelegate);
838
+                }
839
+
840
+                // Find all occurrences based on CleanGlobalID (0x23)
841
+                // there will be no exceptions left if $processed is true, but even if it doesn't hurt to recheck
842
+                $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
843
+                if (is_array($items)) {
844
+                    // Save all existing occurrence as exceptions
845
+                    foreach ($items as $entryid) {
846
+                        // Open occurrence
847
+                        $occurrenceItem = mapi_msgstore_openentry($store, $entryid);
848
+
849
+                        // Save occurrence into main recurring item as exception
850
+                        if ($occurrenceItem) {
851
+                            $occurrenceItemProps = mapi_getprops($occurrenceItem, [$this->proptags['goid'], $this->proptags['recurring']]);
852
+
853
+                            // Find basedate of occurrence item
854
+                            $basedate = $this->getBasedateFromGlobalID($occurrenceItemProps[$this->proptags['goid']]);
855
+                            if ($basedate && $occurrenceItemProps[$this->proptags['recurring']] != true) {
856
+                                $this->mergeException($calendarItem, $occurrenceItem, $basedate, $store);
857
+                            }
858
+                        }
859
+                    }
860
+                }
861
+
862
+                mapi_savechanges($calendarItem);
863
+
864
+                // After applying update of organiser all local categories of occurrence was removed,
865
+                // So if local categories exist then apply it on respective occurrence.
866
+                if (!empty($localCategories)) {
867
+                    $this->applyLocalCategories($calendarItem, $store, $localCategories);
868
+                }
869
+
870
+                if ($move) {
871
+                    // open wastebasket of currently logged in user and move the meeting request to it
872
+                    // for delegates this will be delegate's wastebasket folder
873
+                    $wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
874
+                    mapi_folder_copymessages($calFolder, [$props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
875
+                }
876
+
877
+                $entryid = $props[PR_ENTRYID];
878
+            }
879
+            else {
880
+                /**
881
+                 * This meeting request is not recurring, so can be an exception or normal meeting.
882
+                 * If exception then find main recurring item and update exception
883
+                 * If main recurring item is not found then put exception into Calendar as normal meeting.
884
+                 */
885
+                $calendarItem = false;
886
+
887
+                // We found basedate in GlobalID of this meeting request, so this meeting request if for an occurrence.
888
+                if ($basedate) {
889
+                    // Find main recurring item from CleanGlobalID of this meeting request
890
+                    $items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
891
+                    if (is_array($items)) {
892
+                        foreach ($items as $key => $entryid) {
893
+                            $calendarItem = mapi_msgstore_openentry($store, $entryid);
894
+                        }
895
+                    }
896
+
897
+                    // Main recurring item is found, so now update exception
898
+                    if ($calendarItem) {
899
+                        $this->acceptException($calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate);
900
+                        $calendarItemProps = mapi_getprops($calendarItem, [PR_ENTRYID]);
901
+                        $entryid = $calendarItemProps[PR_ENTRYID];
902
+                    }
903
+                }
904
+
905
+                if (!$calendarItem) {
906
+                    $items = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
907
+                    if (is_array($items)) {
908
+                        // Get local categories before deleting MR.
909
+                        $message = mapi_msgstore_openentry($store, $items[0]);
910
+                        $localCategories = mapi_getprops($message, [$this->proptags['categories']]);
911
+                        mapi_folder_deletemessages($calFolder, $items);
912
+                    }
913
+
914
+                    if ($move) {
915
+                        // All we have to do is open the default calendar,
916
+                        // set the message class correctly to be an appointment item
917
+                        // and move it to the calendar folder
918
+                        $sourcefolder = $this->openParentFolder();
919
+
920
+                        // create a new calendar message, and copy the message to there,
921
+                        // since we want to delete (move to wastebasket) the original message
922
+                        $old_entryid = mapi_getprops($this->message, [PR_ENTRYID]);
923
+                        $calmsg = mapi_folder_createmessage($calFolder);
924
+                        mapi_copyto($this->message, [], [], $calmsg); /* includes attachments and recipients */
925
+
926
+                        // After creating new MR, If local categories exist then apply it on new MR.
927
+                        if (!empty($localCategories)) {
928
+                            mapi_setprops($calmsg, $localCategories);
929
+                        }
930
+
931
+                        $calItemProps = [];
932
+                        $calItemProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
933
+
934
+                        /*
935 935
 						 * the client which has sent this meeting request can generate wrong flagdueby
936 936
 						 * time (mainly OL), so regenerate that property so we will always show reminder
937 937
 						 * on right time
938 938
 						 */
939
-						if (isset($messageprops[$this->proptags['reminderminutes']])) {
940
-							$calItemProps[$this->proptags['flagdueby']] = $messageprops[$this->proptags['startdate']] - ($messageprops[$this->proptags['reminderminutes']] * 60);
941
-						}
942
-
943
-						if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
944
-							if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) {
945
-								$calItemProps[$this->proptags['busystatus']] = fbTentative;
946
-							}
947
-							else {
948
-								$calItemProps[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
949
-							}
950
-							$calItemProps[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
951
-						}
952
-						else {
953
-							$calItemProps[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
954
-						}
955
-
956
-						// when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
957
-						$calItemProps[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
958
-						if ($userAction) {
959
-							$addrInfo = $this->getOwnerAddress($this->store);
960
-
961
-							// if user has responded then set replytime and name
962
-							$calItemProps[$this->proptags['replytime']] = time();
963
-							if (!empty($addrInfo)) {
964
-								$calItemProps[$this->proptags['apptreplyname']] = $addrInfo[0];
965
-							}
966
-						}
967
-
968
-						mapi_setprops($calmsg, $proposeNewTimeProps + $calItemProps);
969
-
970
-						// get properties which stores owner information in meeting request mails
971
-						$props = mapi_getprops($calmsg, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY]);
972
-
973
-						// add owner to recipient table
974
-						$recips = [];
975
-						$this->addOrganizer($props, $recips);
976
-						mapi_message_modifyrecipients($calmsg, MODRECIP_ADD, $recips);
977
-						mapi_savechanges($calmsg);
978
-
979
-						// Move the message to the wastebasket
980
-						$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
981
-						mapi_folder_copymessages($sourcefolder, [$old_entryid[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
982
-
983
-						$messageprops = mapi_getprops($calmsg, [PR_ENTRYID]);
984
-						$entryid = $messageprops[PR_ENTRYID];
985
-					}
986
-					else {
987
-						// Create a new appointment with duplicate properties and recipient, but as an IPM.Appointment
988
-						$new = mapi_folder_createmessage($calFolder);
989
-						$props = mapi_getprops($this->message);
990
-
991
-						$props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
992
-
993
-						// After creating new MR, If local categories exist then apply it on new MR.
994
-						if (!empty($localCategories)) {
995
-							mapi_setprops($new, $localCategories);
996
-						}
997
-
998
-						/*
939
+                        if (isset($messageprops[$this->proptags['reminderminutes']])) {
940
+                            $calItemProps[$this->proptags['flagdueby']] = $messageprops[$this->proptags['startdate']] - ($messageprops[$this->proptags['reminderminutes']] * 60);
941
+                        }
942
+
943
+                        if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
944
+                            if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) {
945
+                                $calItemProps[$this->proptags['busystatus']] = fbTentative;
946
+                            }
947
+                            else {
948
+                                $calItemProps[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
949
+                            }
950
+                            $calItemProps[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
951
+                        }
952
+                        else {
953
+                            $calItemProps[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
954
+                        }
955
+
956
+                        // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
957
+                        $calItemProps[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
958
+                        if ($userAction) {
959
+                            $addrInfo = $this->getOwnerAddress($this->store);
960
+
961
+                            // if user has responded then set replytime and name
962
+                            $calItemProps[$this->proptags['replytime']] = time();
963
+                            if (!empty($addrInfo)) {
964
+                                $calItemProps[$this->proptags['apptreplyname']] = $addrInfo[0];
965
+                            }
966
+                        }
967
+
968
+                        mapi_setprops($calmsg, $proposeNewTimeProps + $calItemProps);
969
+
970
+                        // get properties which stores owner information in meeting request mails
971
+                        $props = mapi_getprops($calmsg, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY]);
972
+
973
+                        // add owner to recipient table
974
+                        $recips = [];
975
+                        $this->addOrganizer($props, $recips);
976
+                        mapi_message_modifyrecipients($calmsg, MODRECIP_ADD, $recips);
977
+                        mapi_savechanges($calmsg);
978
+
979
+                        // Move the message to the wastebasket
980
+                        $wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
981
+                        mapi_folder_copymessages($sourcefolder, [$old_entryid[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
982
+
983
+                        $messageprops = mapi_getprops($calmsg, [PR_ENTRYID]);
984
+                        $entryid = $messageprops[PR_ENTRYID];
985
+                    }
986
+                    else {
987
+                        // Create a new appointment with duplicate properties and recipient, but as an IPM.Appointment
988
+                        $new = mapi_folder_createmessage($calFolder);
989
+                        $props = mapi_getprops($this->message);
990
+
991
+                        $props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
992
+
993
+                        // After creating new MR, If local categories exist then apply it on new MR.
994
+                        if (!empty($localCategories)) {
995
+                            mapi_setprops($new, $localCategories);
996
+                        }
997
+
998
+                        /*
999 999
 						 * the client which has sent this meeting request can generate wrong flagdueby
1000 1000
 						 * time (mainly OL), so regenerate that property so we will always show reminder
1001 1001
 						 * on right time
1002 1002
 						 */
1003
-						if (isset($props[$this->proptags['reminderminutes']])) {
1004
-							$props[$this->proptags['flagdueby']] = $props[$this->proptags['startdate']] - ($props[$this->proptags['reminderminutes']] * 60);
1005
-						}
1006
-
1007
-						// When meeting requests are generated by third-party solutions, we might be missing the properties.
1008
-						if (!isset($props[$this->proptags['updatecounter']])) {
1009
-							$props[$this->proptags['updatecounter']] = 0;
1010
-						}
1011
-						if (!isset($props[$this->proptags['recurring']])) {
1012
-							$props[$this->proptags['recurring']] = false;
1013
-						}
1014
-
1015
-						// when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
1016
-						$props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
1017
-
1018
-						if (isset($props[$this->proptags['intendedbusystatus']])) {
1019
-							if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) {
1020
-								$props[$this->proptags['busystatus']] = fbTentative;
1021
-							}
1022
-							else {
1023
-								$props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
1024
-							}
1025
-							// we already have intendedbusystatus value in $props so no need to copy it
1026
-						}
1027
-						else {
1028
-							$props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
1029
-						}
1030
-
1031
-						if ($userAction) {
1032
-							$addrInfo = $this->getOwnerAddress($this->store);
1033
-
1034
-							// if user has responded then set replytime and name
1035
-							$props[$this->proptags['replytime']] = time();
1036
-							if (!empty($addrInfo)) {
1037
-								$props[$this->proptags['apptreplyname']] = $addrInfo[0];
1038
-							}
1039
-						}
1040
-
1041
-						mapi_setprops($new, $proposeNewTimeProps + $props);
1042
-
1043
-						// Copy attachments too
1044
-						$this->replaceAttachments($this->message, $new);
1045
-
1046
-						// get recipient table of source message
1047
-						$recipientTable = mapi_message_getrecipienttable($this->message);
1048
-
1049
-						// If delegate, then do not add the delegate in recipients
1050
-						if ($isDelegate) {
1051
-							$delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
1052
-							$res = [RES_PROPERTY, [
1053
-								RELOP => RELOP_NE,
1054
-								ULPROPTAG => PR_EMAIL_ADDRESS,
1055
-								VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
1056
-							],
1057
-							];
1058
-							$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops, $res);
1059
-						}
1060
-						else {
1061
-							$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops);
1062
-						}
1063
-						$this->addOrganizer($props, $recipients);
1064
-						mapi_message_modifyrecipients($new, MODRECIP_ADD, $recipients);
1065
-						mapi_savechanges($new);
1066
-
1067
-						$props = mapi_getprops($new, [PR_ENTRYID]);
1068
-						$entryid = $props[PR_ENTRYID];
1069
-					}
1070
-				}
1071
-			}
1072
-		}
1073
-		else {
1074
-			// Here only properties are set on calendaritem, because user is responding from calendar.
1075
-			$props = [];
1076
-			$props[$this->proptags['responsestatus']] = $tentative ? olResponseTentative : olResponseAccepted;
1077
-
1078
-			if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
1079
-				if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) {
1080
-					$props[$this->proptags['busystatus']] = fbTentative;
1081
-				}
1082
-				else {
1083
-					$props[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1084
-				}
1085
-				$props[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1086
-			}
1087
-			else {
1088
-				$props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
1089
-			}
1090
-
1091
-			$props[$this->proptags['meetingstatus']] = olMeetingReceived;
1092
-
1093
-			$addrInfo = $this->getOwnerAddress($this->store);
1094
-
1095
-			// if user has responded then set replytime and name
1096
-			$props[$this->proptags['replytime']] = time();
1097
-			if (!empty($addrInfo)) {
1098
-				$props[$this->proptags['apptreplyname']] = $addrInfo[0];
1099
-			}
1100
-
1101
-			if ($basedate) {
1102
-				$recurr = new Recurrence($store, $this->message);
1103
-
1104
-				// Copy recipients list
1105
-				$reciptable = mapi_message_getrecipienttable($this->message);
1106
-				$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
1107
-
1108
-				if ($recurr->isException($basedate)) {
1109
-					$recurr->modifyException($proposeNewTimeProps + $props, $basedate, $recips);
1110
-				}
1111
-				else {
1112
-					$props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
1113
-					$props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
1114
-
1115
-					$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1116
-					$props[PR_SENT_REPRESENTING_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
1117
-					$props[PR_SENT_REPRESENTING_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
1118
-					$props[PR_SENT_REPRESENTING_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
1119
-					$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
1120
-
1121
-					$recurr->createException($proposeNewTimeProps + $props, $basedate, false, $recips);
1122
-				}
1123
-			}
1124
-			else {
1125
-				mapi_setprops($this->message, $proposeNewTimeProps + $props);
1126
-			}
1127
-			mapi_savechanges($this->message);
1128
-
1129
-			$entryid = $messageprops[PR_ENTRYID];
1130
-		}
1131
-
1132
-		return $entryid;
1133
-	}
1134
-
1135
-	/**
1136
-	 * Declines the meeting request by moving the item to the deleted
1137
-	 * items folder and sending a decline message. After declining, you
1138
-	 * can't use this class instance any more. The message is closed.
1139
-	 * When an occurrence is decline then false is returned because that
1140
-	 * occurrence is deleted not the recurring item.
1141
-	 *
1142
-	 * @param bool   $sendresponse true if a response has to be sent to organizer
1143
-	 * @param string $basedate     if specified contains starttime of day of an occurrence
1144
-	 * @param mixed  $body
1145
-	 *
1146
-	 * @return bool true if item is deleted from Calendar else false
1147
-	 */
1148
-	public function doDecline($sendresponse, $basedate = false, $body = false) {
1149
-		if ($this->isLocalOrganiser()) {
1150
-			return false;
1151
-		}
1152
-
1153
-		$result = false;
1154
-		$calendaritem = false;
1155
-
1156
-		// Remove any previous calendar items with this goid and appt id
1157
-		$messageprops = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
1158
-
1159
-		// If this meeting request is received by a delegate then open delegator's store.
1160
-		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
1161
-			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
1162
-
1163
-			$store = $delegatorStore['store'];
1164
-			$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
1165
-		}
1166
-		else {
1167
-			$calFolder = $this->openDefaultCalendar();
1168
-			$store = $this->store;
1169
-		}
1170
-
1171
-		// check for calendar access before deleting the calendar item
1172
-		if ($this->checkCalendarWriteAccess($store) !== true) {
1173
-			// Throw an exception that we don't have write permissions on calendar folder,
1174
-			// allow caller to fill the error message
1175
-			throw new MAPIException(null, MAPI_E_NO_ACCESS);
1176
-		}
1177
-
1178
-		$goid = $messageprops[$this->proptags['goid']];
1179
-
1180
-		// First, find the items in the calendar by GlobalObjid (0x3)
1181
-		$entryids = $this->findCalendarItems($goid, $calFolder);
1182
-
1183
-		if (!$basedate) {
1184
-			$basedate = $this->getBasedateFromGlobalID($goid);
1185
-		}
1186
-
1187
-		if ($sendresponse) {
1188
-			$this->createResponse(olResponseDeclined, [], $body, $store, $basedate, $calFolder);
1189
-		}
1190
-
1191
-		if ($basedate) {
1192
-			// use CleanGlobalObjid (0x23)
1193
-			$calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
1194
-
1195
-			if (is_array($calendaritems)) {
1196
-				foreach ($calendaritems as $entryid) {
1197
-					// Open each calendar item and set the properties of the cancellation object
1198
-					$calendaritem = mapi_msgstore_openentry($store, $entryid);
1199
-
1200
-					// Recurring item is found, now delete exception
1201
-					if ($calendaritem) {
1202
-						$this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store);
1203
-						$result = true;
1204
-					}
1205
-				}
1206
-			}
1207
-
1208
-			if ($this->isMeetingRequest()) {
1209
-				$calendaritem = false;
1210
-			}
1211
-		}
1212
-
1213
-		if (!$calendaritem) {
1214
-			$calendar = $this->openDefaultCalendar($store);
1215
-
1216
-			if (!empty($entryids)) {
1217
-				mapi_folder_deletemessages($calendar, $entryids);
1218
-			}
1219
-
1220
-			// All we have to do to decline, is to move the item to the waste basket
1221
-			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1222
-			$sourcefolder = $this->openParentFolder();
1223
-
1224
-			$messageprops = mapi_getprops($this->message, [PR_ENTRYID]);
1225
-
1226
-			// Release the message
1227
-			$this->message = null;
1228
-
1229
-			// Move the message to the waste basket
1230
-			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1231
-
1232
-			$result = true;
1233
-		}
1234
-
1235
-		return $result;
1236
-	}
1237
-
1238
-	/**
1239
-	 * Removes a meeting request from the calendar when the user presses the
1240
-	 * 'remove from calendar' button in response to a meeting cancellation.
1241
-	 *
1242
-	 * @param string $basedate if specified contains starttime of day of an occurrence
1243
-	 */
1244
-	public function doRemoveFromCalendar($basedate) {
1245
-		if ($this->isLocalOrganiser()) {
1246
-			return false;
1247
-		}
1248
-
1249
-		$messageprops = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['goid'], PR_RCVD_REPRESENTING_ENTRYID, PR_MESSAGE_CLASS]);
1250
-
1251
-		$goid = $messageprops[$this->proptags['goid']];
1252
-
1253
-		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
1254
-			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
1255
-
1256
-			$store = $delegatorStore['store'];
1257
-			$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
1258
-		}
1259
-		else {
1260
-			$store = $this->store;
1261
-			$calFolder = $this->openDefaultCalendar();
1262
-		}
1263
-
1264
-		// check for calendar access before deleting the calendar item
1265
-		if ($this->checkCalendarWriteAccess($store) !== true) {
1266
-			// Throw an exception that we don't have write permissions on calendar folder,
1267
-			// allow caller to fill the error message
1268
-			throw new MAPIException(null, MAPI_E_NO_ACCESS);
1269
-		}
1270
-
1271
-		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1272
-		// get the source folder of the meeting message
1273
-		$sourcefolder = $this->openParentFolder();
1274
-
1275
-		// Check if the message is a meeting request in the inbox or a calendaritem by checking the message class
1276
-		if ($this->isMeetingCancellation($messageprops[PR_MESSAGE_CLASS])) {
1277
-			// get the basedate to check for exception
1278
-			$basedate = $this->getBasedateFromGlobalID($goid);
1279
-
1280
-			$calendarItem = $this->getCorrespondentCalendarItem(true);
1281
-
1282
-			if ($calendarItem !== false) {
1283
-				// basedate is provided so open exception
1284
-				if ($basedate) {
1285
-					$exception = $this->getExceptionItem($calendarItem, $basedate);
1286
-
1287
-					if ($exception !== false) {
1288
-						// exception found, remove it from calendar
1289
-						$this->doRemoveExceptionFromCalendar($basedate, $calendarItem, $store);
1290
-					}
1291
-				}
1292
-				else {
1293
-					// remove normal / recurring series from calendar
1294
-					$entryids = mapi_getprops($calendarItem, [PR_ENTRYID]);
1295
-
1296
-					$entryids = [$entryids[PR_ENTRYID]];
1297
-
1298
-					mapi_folder_copymessages($calFolder, $entryids, $wastebasket, MESSAGE_MOVE);
1299
-				}
1300
-			}
1301
-
1302
-			// Release the message, because we are going to move it to wastebasket
1303
-			$this->message = null;
1304
-
1305
-			// Move the cancellation mail to wastebasket
1306
-			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1307
-		}
1308
-		else {
1309
-			// Here only properties are set on calendaritem, because user is responding from calendar.
1310
-			if ($basedate) {
1311
-				// remove the occurrence
1312
-				$this->doRemoveExceptionFromCalendar($basedate, $this->message, $store);
1313
-			}
1314
-			else {
1315
-				// remove normal/recurring meeting item.
1316
-				// Move the message to the waste basket
1317
-				mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1318
-			}
1319
-		}
1320
-	}
1321
-
1322
-	/**
1323
-	 * Function can be used to cancel any existing meeting and send cancellation mails to attendees.
1324
-	 * Should only be called from meeting object from calendar.
1325
-	 *
1326
-	 * @param string $basedate (optional) basedate of occurrence which should be cancelled
1327
-	 * @FIXME cancellation mail is also sent to attendee which has declined the meeting
1328
-	 * @FIXME don't send canellation mail when cancelling meeting from past
1329
-	 */
1330
-	public function doCancelInvitation($basedate = false) {
1331
-		if (!$this->isLocalOrganiser()) {
1332
-			return;
1333
-		}
1334
-
1335
-		// check write access for delegate
1336
-		if ($this->checkCalendarWriteAccess($this->store) !== true) {
1337
-			// Throw an exception that we don't have write permissions on calendar folder,
1338
-			// error message will be filled by module
1339
-			throw new MAPIException(null, MAPI_E_NO_ACCESS);
1340
-		}
1341
-
1342
-		$messageProps = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['recurring']]);
1343
-
1344
-		if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
1345
-			// cancellation of recurring series or one occurrence
1346
-			$recurrence = new Recurrence($this->store, $this->message);
1347
-
1348
-			// if basedate is specified then we are cancelling only one occurrence, so create exception for that occurrence
1349
-			if ($basedate) {
1350
-				$recurrence->createException([], $basedate, true);
1351
-			}
1352
-
1353
-			// update the meeting request
1354
-			$this->updateMeetingRequest();
1355
-
1356
-			// send cancellation mails
1357
-			$this->sendMeetingRequest(true, _('Canceled: '), $basedate);
1358
-
1359
-			// save changes in the message
1360
-			mapi_savechanges($this->message);
1361
-		}
1362
-		else {
1363
-			// cancellation of normal meeting request
1364
-			// Send the cancellation
1365
-			$this->updateMeetingRequest();
1366
-			$this->sendMeetingRequest(true, _('Canceled: '));
1367
-
1368
-			// save changes in the message
1369
-			mapi_savechanges($this->message);
1370
-		}
1371
-
1372
-		// if basedate is specified then we have already created exception of it so nothing should be done now
1373
-		// but when cancelling normal / recurring meeting request we need to remove meeting from calendar
1374
-		if ($basedate === false) {
1375
-			// get the wastebasket folder, for delegate this will give wastebasket of delegate
1376
-			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1377
-
1378
-			// get the source folder of the meeting message
1379
-			$sourcefolder = $this->openParentFolder();
1380
-
1381
-			// Move the message to the deleted items
1382
-			mapi_folder_copymessages($sourcefolder, [$messageProps[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1383
-		}
1384
-	}
1385
-
1386
-	/**
1387
-	 * Convert epoch to MAPI FileTime, number of 100-nanosecond units since
1388
-	 * the start of January 1, 1601.
1389
-	 * https://msdn.microsoft.com/en-us/library/office/cc765906.aspx.
1390
-	 *
1391
-	 * @param int the current epoch
1392
-	 * @param mixed $epoch
1393
-	 *
1394
-	 * @return the MAPI FileTime equalevent to the given epoch time
1395
-	 */
1396
-	public function epochToMapiFileTime($epoch) {
1397
-		$nanoseconds_between_epoch = 116444736000000000;
1398
-
1399
-		return ($epoch * 10000000) + $nanoseconds_between_epoch;
1400
-	}
1401
-
1402
-	/**
1403
-	 * Sets the properties in the message so that is can be sent
1404
-	 * as a meeting request. The caller has to submit the message. This
1405
-	 * is only used for new MeetingRequests. Pass the appointment item as $message
1406
-	 * in the constructor to do this.
1407
-	 *
1408
-	 * @param mixed $basedate
1409
-	 */
1410
-	public function setMeetingRequest($basedate = false) {
1411
-		$props = mapi_getprops($this->message, [$this->proptags['updatecounter']]);
1412
-
1413
-		// Create a new global id for this item
1414
-		// https://msdn.microsoft.com/en-us/library/ee160198(v=exchg.80).aspx
1415
-		$goid = pack('H*', '040000008200E00074C5B7101A82E00800000000');
1416
-		// Creation Time
1417
-		$time = $this->epochToMapiFileTime(time());
1418
-		$highdatetime = $time >> 32;
1419
-		$lowdatetime = $time & 0xFFFFFFFF;
1420
-		$goid .= pack('II', $lowdatetime, $highdatetime);
1421
-		// 8 Zeros
1422
-		$goid .= pack('P', 0);
1423
-		// Length of the random data
1424
-		$goid .= pack('V', 16);
1425
-		// Random data.
1426
-		for ($i = 0; $i < 16; ++$i) {
1427
-			$goid .= chr(rand(0, 255));
1428
-		}
1429
-
1430
-		// Create a new appointment id for this item
1431
-		$apptid = rand();
1432
-
1433
-		$props[PR_OWNER_APPT_ID] = $apptid;
1434
-		$props[PR_ICON_INDEX] = 1026;
1435
-		$props[$this->proptags['goid']] = $goid;
1436
-		$props[$this->proptags['goid2']] = $goid;
1437
-
1438
-		if (!isset($props[$this->proptags['updatecounter']])) {
1439
-			$props[$this->proptags['updatecounter']] = 0;            // OL also starts sequence no with zero.
1440
-			$props[$this->proptags['last_updatecounter']] = 0;
1441
-		}
1442
-
1443
-		mapi_setprops($this->message, $props);
1444
-	}
1445
-
1446
-	/**
1447
-	 * Sends a meeting request by copying it to the outbox, converting
1448
-	 * the message class, adding some properties that are required only
1449
-	 * for sending the message and submitting the message. Set cancel to
1450
-	 * true if you wish to completely cancel the meeting request. You can
1451
-	 * specify an optional 'prefix' to prefix the sent message, which is normally
1452
-	 * 'Canceled: '.
1453
-	 *
1454
-	 * @param mixed $cancel
1455
-	 * @param mixed $prefix
1456
-	 * @param mixed $basedate
1457
-	 * @param mixed $modifiedRecips
1458
-	 * @param mixed $deletedRecips
1459
-	 */
1460
-	public function sendMeetingRequest($cancel, $prefix = false, $basedate = false, $modifiedRecips = false, $deletedRecips = false) {
1461
-		$this->includesResources = false;
1462
-		$this->nonAcceptingResources = [];
1463
-
1464
-		// Get the properties of the message
1465
-		$messageprops = mapi_getprops($this->message, [$this->proptags['recurring']]);
1466
-
1467
-		/*
1003
+                        if (isset($props[$this->proptags['reminderminutes']])) {
1004
+                            $props[$this->proptags['flagdueby']] = $props[$this->proptags['startdate']] - ($props[$this->proptags['reminderminutes']] * 60);
1005
+                        }
1006
+
1007
+                        // When meeting requests are generated by third-party solutions, we might be missing the properties.
1008
+                        if (!isset($props[$this->proptags['updatecounter']])) {
1009
+                            $props[$this->proptags['updatecounter']] = 0;
1010
+                        }
1011
+                        if (!isset($props[$this->proptags['recurring']])) {
1012
+                            $props[$this->proptags['recurring']] = false;
1013
+                        }
1014
+
1015
+                        // when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
1016
+                        $props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
1017
+
1018
+                        if (isset($props[$this->proptags['intendedbusystatus']])) {
1019
+                            if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) {
1020
+                                $props[$this->proptags['busystatus']] = fbTentative;
1021
+                            }
1022
+                            else {
1023
+                                $props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
1024
+                            }
1025
+                            // we already have intendedbusystatus value in $props so no need to copy it
1026
+                        }
1027
+                        else {
1028
+                            $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
1029
+                        }
1030
+
1031
+                        if ($userAction) {
1032
+                            $addrInfo = $this->getOwnerAddress($this->store);
1033
+
1034
+                            // if user has responded then set replytime and name
1035
+                            $props[$this->proptags['replytime']] = time();
1036
+                            if (!empty($addrInfo)) {
1037
+                                $props[$this->proptags['apptreplyname']] = $addrInfo[0];
1038
+                            }
1039
+                        }
1040
+
1041
+                        mapi_setprops($new, $proposeNewTimeProps + $props);
1042
+
1043
+                        // Copy attachments too
1044
+                        $this->replaceAttachments($this->message, $new);
1045
+
1046
+                        // get recipient table of source message
1047
+                        $recipientTable = mapi_message_getrecipienttable($this->message);
1048
+
1049
+                        // If delegate, then do not add the delegate in recipients
1050
+                        if ($isDelegate) {
1051
+                            $delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
1052
+                            $res = [RES_PROPERTY, [
1053
+                                RELOP => RELOP_NE,
1054
+                                ULPROPTAG => PR_EMAIL_ADDRESS,
1055
+                                VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
1056
+                            ],
1057
+                            ];
1058
+                            $recipients = mapi_table_queryallrows($recipientTable, $this->recipprops, $res);
1059
+                        }
1060
+                        else {
1061
+                            $recipients = mapi_table_queryallrows($recipientTable, $this->recipprops);
1062
+                        }
1063
+                        $this->addOrganizer($props, $recipients);
1064
+                        mapi_message_modifyrecipients($new, MODRECIP_ADD, $recipients);
1065
+                        mapi_savechanges($new);
1066
+
1067
+                        $props = mapi_getprops($new, [PR_ENTRYID]);
1068
+                        $entryid = $props[PR_ENTRYID];
1069
+                    }
1070
+                }
1071
+            }
1072
+        }
1073
+        else {
1074
+            // Here only properties are set on calendaritem, because user is responding from calendar.
1075
+            $props = [];
1076
+            $props[$this->proptags['responsestatus']] = $tentative ? olResponseTentative : olResponseAccepted;
1077
+
1078
+            if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
1079
+                if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) {
1080
+                    $props[$this->proptags['busystatus']] = fbTentative;
1081
+                }
1082
+                else {
1083
+                    $props[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1084
+                }
1085
+                $props[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1086
+            }
1087
+            else {
1088
+                $props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
1089
+            }
1090
+
1091
+            $props[$this->proptags['meetingstatus']] = olMeetingReceived;
1092
+
1093
+            $addrInfo = $this->getOwnerAddress($this->store);
1094
+
1095
+            // if user has responded then set replytime and name
1096
+            $props[$this->proptags['replytime']] = time();
1097
+            if (!empty($addrInfo)) {
1098
+                $props[$this->proptags['apptreplyname']] = $addrInfo[0];
1099
+            }
1100
+
1101
+            if ($basedate) {
1102
+                $recurr = new Recurrence($store, $this->message);
1103
+
1104
+                // Copy recipients list
1105
+                $reciptable = mapi_message_getrecipienttable($this->message);
1106
+                $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
1107
+
1108
+                if ($recurr->isException($basedate)) {
1109
+                    $recurr->modifyException($proposeNewTimeProps + $props, $basedate, $recips);
1110
+                }
1111
+                else {
1112
+                    $props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
1113
+                    $props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
1114
+
1115
+                    $props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1116
+                    $props[PR_SENT_REPRESENTING_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
1117
+                    $props[PR_SENT_REPRESENTING_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
1118
+                    $props[PR_SENT_REPRESENTING_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
1119
+                    $props[PR_SENT_REPRESENTING_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
1120
+
1121
+                    $recurr->createException($proposeNewTimeProps + $props, $basedate, false, $recips);
1122
+                }
1123
+            }
1124
+            else {
1125
+                mapi_setprops($this->message, $proposeNewTimeProps + $props);
1126
+            }
1127
+            mapi_savechanges($this->message);
1128
+
1129
+            $entryid = $messageprops[PR_ENTRYID];
1130
+        }
1131
+
1132
+        return $entryid;
1133
+    }
1134
+
1135
+    /**
1136
+     * Declines the meeting request by moving the item to the deleted
1137
+     * items folder and sending a decline message. After declining, you
1138
+     * can't use this class instance any more. The message is closed.
1139
+     * When an occurrence is decline then false is returned because that
1140
+     * occurrence is deleted not the recurring item.
1141
+     *
1142
+     * @param bool   $sendresponse true if a response has to be sent to organizer
1143
+     * @param string $basedate     if specified contains starttime of day of an occurrence
1144
+     * @param mixed  $body
1145
+     *
1146
+     * @return bool true if item is deleted from Calendar else false
1147
+     */
1148
+    public function doDecline($sendresponse, $basedate = false, $body = false) {
1149
+        if ($this->isLocalOrganiser()) {
1150
+            return false;
1151
+        }
1152
+
1153
+        $result = false;
1154
+        $calendaritem = false;
1155
+
1156
+        // Remove any previous calendar items with this goid and appt id
1157
+        $messageprops = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
1158
+
1159
+        // If this meeting request is received by a delegate then open delegator's store.
1160
+        if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
1161
+            $delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
1162
+
1163
+            $store = $delegatorStore['store'];
1164
+            $calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
1165
+        }
1166
+        else {
1167
+            $calFolder = $this->openDefaultCalendar();
1168
+            $store = $this->store;
1169
+        }
1170
+
1171
+        // check for calendar access before deleting the calendar item
1172
+        if ($this->checkCalendarWriteAccess($store) !== true) {
1173
+            // Throw an exception that we don't have write permissions on calendar folder,
1174
+            // allow caller to fill the error message
1175
+            throw new MAPIException(null, MAPI_E_NO_ACCESS);
1176
+        }
1177
+
1178
+        $goid = $messageprops[$this->proptags['goid']];
1179
+
1180
+        // First, find the items in the calendar by GlobalObjid (0x3)
1181
+        $entryids = $this->findCalendarItems($goid, $calFolder);
1182
+
1183
+        if (!$basedate) {
1184
+            $basedate = $this->getBasedateFromGlobalID($goid);
1185
+        }
1186
+
1187
+        if ($sendresponse) {
1188
+            $this->createResponse(olResponseDeclined, [], $body, $store, $basedate, $calFolder);
1189
+        }
1190
+
1191
+        if ($basedate) {
1192
+            // use CleanGlobalObjid (0x23)
1193
+            $calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
1194
+
1195
+            if (is_array($calendaritems)) {
1196
+                foreach ($calendaritems as $entryid) {
1197
+                    // Open each calendar item and set the properties of the cancellation object
1198
+                    $calendaritem = mapi_msgstore_openentry($store, $entryid);
1199
+
1200
+                    // Recurring item is found, now delete exception
1201
+                    if ($calendaritem) {
1202
+                        $this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store);
1203
+                        $result = true;
1204
+                    }
1205
+                }
1206
+            }
1207
+
1208
+            if ($this->isMeetingRequest()) {
1209
+                $calendaritem = false;
1210
+            }
1211
+        }
1212
+
1213
+        if (!$calendaritem) {
1214
+            $calendar = $this->openDefaultCalendar($store);
1215
+
1216
+            if (!empty($entryids)) {
1217
+                mapi_folder_deletemessages($calendar, $entryids);
1218
+            }
1219
+
1220
+            // All we have to do to decline, is to move the item to the waste basket
1221
+            $wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1222
+            $sourcefolder = $this->openParentFolder();
1223
+
1224
+            $messageprops = mapi_getprops($this->message, [PR_ENTRYID]);
1225
+
1226
+            // Release the message
1227
+            $this->message = null;
1228
+
1229
+            // Move the message to the waste basket
1230
+            mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1231
+
1232
+            $result = true;
1233
+        }
1234
+
1235
+        return $result;
1236
+    }
1237
+
1238
+    /**
1239
+     * Removes a meeting request from the calendar when the user presses the
1240
+     * 'remove from calendar' button in response to a meeting cancellation.
1241
+     *
1242
+     * @param string $basedate if specified contains starttime of day of an occurrence
1243
+     */
1244
+    public function doRemoveFromCalendar($basedate) {
1245
+        if ($this->isLocalOrganiser()) {
1246
+            return false;
1247
+        }
1248
+
1249
+        $messageprops = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['goid'], PR_RCVD_REPRESENTING_ENTRYID, PR_MESSAGE_CLASS]);
1250
+
1251
+        $goid = $messageprops[$this->proptags['goid']];
1252
+
1253
+        if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
1254
+            $delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
1255
+
1256
+            $store = $delegatorStore['store'];
1257
+            $calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
1258
+        }
1259
+        else {
1260
+            $store = $this->store;
1261
+            $calFolder = $this->openDefaultCalendar();
1262
+        }
1263
+
1264
+        // check for calendar access before deleting the calendar item
1265
+        if ($this->checkCalendarWriteAccess($store) !== true) {
1266
+            // Throw an exception that we don't have write permissions on calendar folder,
1267
+            // allow caller to fill the error message
1268
+            throw new MAPIException(null, MAPI_E_NO_ACCESS);
1269
+        }
1270
+
1271
+        $wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1272
+        // get the source folder of the meeting message
1273
+        $sourcefolder = $this->openParentFolder();
1274
+
1275
+        // Check if the message is a meeting request in the inbox or a calendaritem by checking the message class
1276
+        if ($this->isMeetingCancellation($messageprops[PR_MESSAGE_CLASS])) {
1277
+            // get the basedate to check for exception
1278
+            $basedate = $this->getBasedateFromGlobalID($goid);
1279
+
1280
+            $calendarItem = $this->getCorrespondentCalendarItem(true);
1281
+
1282
+            if ($calendarItem !== false) {
1283
+                // basedate is provided so open exception
1284
+                if ($basedate) {
1285
+                    $exception = $this->getExceptionItem($calendarItem, $basedate);
1286
+
1287
+                    if ($exception !== false) {
1288
+                        // exception found, remove it from calendar
1289
+                        $this->doRemoveExceptionFromCalendar($basedate, $calendarItem, $store);
1290
+                    }
1291
+                }
1292
+                else {
1293
+                    // remove normal / recurring series from calendar
1294
+                    $entryids = mapi_getprops($calendarItem, [PR_ENTRYID]);
1295
+
1296
+                    $entryids = [$entryids[PR_ENTRYID]];
1297
+
1298
+                    mapi_folder_copymessages($calFolder, $entryids, $wastebasket, MESSAGE_MOVE);
1299
+                }
1300
+            }
1301
+
1302
+            // Release the message, because we are going to move it to wastebasket
1303
+            $this->message = null;
1304
+
1305
+            // Move the cancellation mail to wastebasket
1306
+            mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1307
+        }
1308
+        else {
1309
+            // Here only properties are set on calendaritem, because user is responding from calendar.
1310
+            if ($basedate) {
1311
+                // remove the occurrence
1312
+                $this->doRemoveExceptionFromCalendar($basedate, $this->message, $store);
1313
+            }
1314
+            else {
1315
+                // remove normal/recurring meeting item.
1316
+                // Move the message to the waste basket
1317
+                mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1318
+            }
1319
+        }
1320
+    }
1321
+
1322
+    /**
1323
+     * Function can be used to cancel any existing meeting and send cancellation mails to attendees.
1324
+     * Should only be called from meeting object from calendar.
1325
+     *
1326
+     * @param string $basedate (optional) basedate of occurrence which should be cancelled
1327
+     * @FIXME cancellation mail is also sent to attendee which has declined the meeting
1328
+     * @FIXME don't send canellation mail when cancelling meeting from past
1329
+     */
1330
+    public function doCancelInvitation($basedate = false) {
1331
+        if (!$this->isLocalOrganiser()) {
1332
+            return;
1333
+        }
1334
+
1335
+        // check write access for delegate
1336
+        if ($this->checkCalendarWriteAccess($this->store) !== true) {
1337
+            // Throw an exception that we don't have write permissions on calendar folder,
1338
+            // error message will be filled by module
1339
+            throw new MAPIException(null, MAPI_E_NO_ACCESS);
1340
+        }
1341
+
1342
+        $messageProps = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['recurring']]);
1343
+
1344
+        if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
1345
+            // cancellation of recurring series or one occurrence
1346
+            $recurrence = new Recurrence($this->store, $this->message);
1347
+
1348
+            // if basedate is specified then we are cancelling only one occurrence, so create exception for that occurrence
1349
+            if ($basedate) {
1350
+                $recurrence->createException([], $basedate, true);
1351
+            }
1352
+
1353
+            // update the meeting request
1354
+            $this->updateMeetingRequest();
1355
+
1356
+            // send cancellation mails
1357
+            $this->sendMeetingRequest(true, _('Canceled: '), $basedate);
1358
+
1359
+            // save changes in the message
1360
+            mapi_savechanges($this->message);
1361
+        }
1362
+        else {
1363
+            // cancellation of normal meeting request
1364
+            // Send the cancellation
1365
+            $this->updateMeetingRequest();
1366
+            $this->sendMeetingRequest(true, _('Canceled: '));
1367
+
1368
+            // save changes in the message
1369
+            mapi_savechanges($this->message);
1370
+        }
1371
+
1372
+        // if basedate is specified then we have already created exception of it so nothing should be done now
1373
+        // but when cancelling normal / recurring meeting request we need to remove meeting from calendar
1374
+        if ($basedate === false) {
1375
+            // get the wastebasket folder, for delegate this will give wastebasket of delegate
1376
+            $wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1377
+
1378
+            // get the source folder of the meeting message
1379
+            $sourcefolder = $this->openParentFolder();
1380
+
1381
+            // Move the message to the deleted items
1382
+            mapi_folder_copymessages($sourcefolder, [$messageProps[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1383
+        }
1384
+    }
1385
+
1386
+    /**
1387
+     * Convert epoch to MAPI FileTime, number of 100-nanosecond units since
1388
+     * the start of January 1, 1601.
1389
+     * https://msdn.microsoft.com/en-us/library/office/cc765906.aspx.
1390
+     *
1391
+     * @param int the current epoch
1392
+     * @param mixed $epoch
1393
+     *
1394
+     * @return the MAPI FileTime equalevent to the given epoch time
1395
+     */
1396
+    public function epochToMapiFileTime($epoch) {
1397
+        $nanoseconds_between_epoch = 116444736000000000;
1398
+
1399
+        return ($epoch * 10000000) + $nanoseconds_between_epoch;
1400
+    }
1401
+
1402
+    /**
1403
+     * Sets the properties in the message so that is can be sent
1404
+     * as a meeting request. The caller has to submit the message. This
1405
+     * is only used for new MeetingRequests. Pass the appointment item as $message
1406
+     * in the constructor to do this.
1407
+     *
1408
+     * @param mixed $basedate
1409
+     */
1410
+    public function setMeetingRequest($basedate = false) {
1411
+        $props = mapi_getprops($this->message, [$this->proptags['updatecounter']]);
1412
+
1413
+        // Create a new global id for this item
1414
+        // https://msdn.microsoft.com/en-us/library/ee160198(v=exchg.80).aspx
1415
+        $goid = pack('H*', '040000008200E00074C5B7101A82E00800000000');
1416
+        // Creation Time
1417
+        $time = $this->epochToMapiFileTime(time());
1418
+        $highdatetime = $time >> 32;
1419
+        $lowdatetime = $time & 0xFFFFFFFF;
1420
+        $goid .= pack('II', $lowdatetime, $highdatetime);
1421
+        // 8 Zeros
1422
+        $goid .= pack('P', 0);
1423
+        // Length of the random data
1424
+        $goid .= pack('V', 16);
1425
+        // Random data.
1426
+        for ($i = 0; $i < 16; ++$i) {
1427
+            $goid .= chr(rand(0, 255));
1428
+        }
1429
+
1430
+        // Create a new appointment id for this item
1431
+        $apptid = rand();
1432
+
1433
+        $props[PR_OWNER_APPT_ID] = $apptid;
1434
+        $props[PR_ICON_INDEX] = 1026;
1435
+        $props[$this->proptags['goid']] = $goid;
1436
+        $props[$this->proptags['goid2']] = $goid;
1437
+
1438
+        if (!isset($props[$this->proptags['updatecounter']])) {
1439
+            $props[$this->proptags['updatecounter']] = 0;            // OL also starts sequence no with zero.
1440
+            $props[$this->proptags['last_updatecounter']] = 0;
1441
+        }
1442
+
1443
+        mapi_setprops($this->message, $props);
1444
+    }
1445
+
1446
+    /**
1447
+     * Sends a meeting request by copying it to the outbox, converting
1448
+     * the message class, adding some properties that are required only
1449
+     * for sending the message and submitting the message. Set cancel to
1450
+     * true if you wish to completely cancel the meeting request. You can
1451
+     * specify an optional 'prefix' to prefix the sent message, which is normally
1452
+     * 'Canceled: '.
1453
+     *
1454
+     * @param mixed $cancel
1455
+     * @param mixed $prefix
1456
+     * @param mixed $basedate
1457
+     * @param mixed $modifiedRecips
1458
+     * @param mixed $deletedRecips
1459
+     */
1460
+    public function sendMeetingRequest($cancel, $prefix = false, $basedate = false, $modifiedRecips = false, $deletedRecips = false) {
1461
+        $this->includesResources = false;
1462
+        $this->nonAcceptingResources = [];
1463
+
1464
+        // Get the properties of the message
1465
+        $messageprops = mapi_getprops($this->message, [$this->proptags['recurring']]);
1466
+
1467
+        /*
1468 1468
 		 * Submit message to non-resource recipients
1469 1469
 		 */
1470
-		// Set BusyStatus to olTentative (1)
1471
-		// Set MeetingStatus to olMeetingReceived
1472
-		// Set ResponseStatus to olResponseNotResponded
1470
+        // Set BusyStatus to olTentative (1)
1471
+        // Set MeetingStatus to olMeetingReceived
1472
+        // Set ResponseStatus to olResponseNotResponded
1473 1473
 
1474
-		/*
1474
+        /*
1475 1475
 		 * While sending recurrence meeting exceptions are not send as attachments
1476 1476
 		 * because first all exceptions are send and then recurrence meeting is sent.
1477 1477
 		 */
1478
-		if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] && !$basedate) {
1479
-			// Book resource
1480
-			$resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
1481
-
1482
-			if (!$this->errorSetResource) {
1483
-				$recurr = new Recurrence($this->openDefaultStore(), $this->message);
1484
-
1485
-				// First send meetingrequest for recurring item
1486
-				$this->submitMeetingRequest($this->message, $cancel, $prefix, false, $recurr, false, $modifiedRecips, $deletedRecips);
1487
-
1488
-				// Then send all meeting request for all exceptions
1489
-				$exceptions = $recurr->getAllExceptions();
1490
-				if ($exceptions) {
1491
-					foreach ($exceptions as $exceptionBasedate) {
1492
-						$attach = $recurr->getExceptionAttachment($exceptionBasedate);
1493
-
1494
-						if ($attach) {
1495
-							$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1496
-							$this->submitMeetingRequest($occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $modifiedRecips, $deletedRecips);
1497
-							mapi_savechanges($attach);
1498
-						}
1499
-					}
1500
-				}
1501
-			}
1502
-		}
1503
-		else {
1504
-			// Basedate found, an exception is to be send
1505
-			if ($basedate) {
1506
-				$recurr = new Recurrence($this->openDefaultStore(), $this->message);
1507
-
1508
-				if ($cancel) {
1509
-					// @TODO: remove occurrence from Resource's Calendar if resource was booked for whole series
1510
-					$this->submitMeetingRequest($this->message, $cancel, $prefix, $basedate, $recurr, false);
1511
-				}
1512
-				else {
1513
-					$attach = $recurr->getExceptionAttachment($basedate);
1514
-
1515
-					if ($attach) {
1516
-						$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1517
-
1518
-						// Book resource for this occurrence
1519
-						$resourceRecipData = $this->bookResources($occurrenceItem, $cancel, $prefix, $basedate);
1520
-
1521
-						if (!$this->errorSetResource) {
1522
-							// Save all previous changes
1523
-							mapi_savechanges($this->message);
1524
-
1525
-							$this->submitMeetingRequest($occurrenceItem, $cancel, $prefix, $basedate, $recurr, true, $modifiedRecips, $deletedRecips);
1526
-							mapi_savechanges($occurrenceItem);
1527
-							mapi_savechanges($attach);
1528
-						}
1529
-					}
1530
-				}
1531
-			}
1532
-			else {
1533
-				// This is normal meeting
1534
-				$resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
1535
-
1536
-				if (!$this->errorSetResource) {
1537
-					$this->submitMeetingRequest($this->message, $cancel, $prefix, false, false, false, $modifiedRecips, $deletedRecips);
1538
-				}
1539
-			}
1540
-		}
1541
-
1542
-		if (isset($this->errorSetResource) && $this->errorSetResource) {
1543
-			return [
1544
-				'error' => $this->errorSetResource,
1545
-				'displayname' => $this->recipientDisplayname,
1546
-			];
1547
-		}
1548
-
1549
-		return true;
1550
-	}
1551
-
1552
-	/**
1553
-	 * This function will get freebusy data for user based on the timeframe passed in arguments.
1554
-	 *
1555
-	 * @param {HexString} $entryID Entryid of the user for which we need to get freebusy data
1556
-	 * @param {Number} $start start offset for freebusy publish range
1557
-	 * @param {Number} $end end offset for freebusy publish range
1558
-	 *
1559
-	 * @return {Array} freebusy blocks for passed publish range
1560
-	 */
1561
-	public function getFreeBusyInfo($entryID, $start, $end) {
1562
-		$result = [];
1563
-		$fbsupport = mapi_freebusysupport_open($this->session);
1564
-
1565
-		$fbDataArray = mapi_freebusysupport_loaddata($fbsupport, [$entryID]);
1566
-
1567
-		if ($fbDataArray[0] != null) {
1568
-			foreach ($fbDataArray as $fbDataUser) {
1569
-				$rangeuser1 = mapi_freebusydata_getpublishrange($fbDataUser);
1570
-				if ($rangeuser1 == null) {
1571
-					return $result;
1572
-				}
1573
-
1574
-				$enumblock = mapi_freebusydata_enumblocks($fbDataUser, $start, $end);
1575
-				mapi_freebusyenumblock_reset($enumblock);
1576
-
1577
-				while (true) {
1578
-					$blocks = mapi_freebusyenumblock_next($enumblock, 100);
1579
-					if (!$blocks) {
1580
-						break;
1581
-					}
1582
-
1583
-					foreach ($blocks as $blockItem) {
1584
-						$result[] = $blockItem;
1585
-					}
1586
-				}
1587
-			}
1588
-		}
1589
-
1590
-		mapi_freebusysupport_close($fbsupport);
1591
-
1592
-		return $result;
1593
-	}
1594
-
1595
-	/**
1596
-	 * Updates the message after an update has been performed (for example,
1597
-	 * changing the time of the meeting). This must be called before re-sending
1598
-	 * the meeting request. You can also call this function instead of 'setMeetingRequest()'
1599
-	 * as it will automatically call setMeetingRequest on this object if it is the first
1600
-	 * call to this function.
1601
-	 *
1602
-	 * @param mixed $basedate
1603
-	 */
1604
-	public function updateMeetingRequest($basedate = false) {
1605
-		$messageprops = mapi_getprops($this->message, [$this->proptags['last_updatecounter'], $this->proptags['goid']]);
1606
-
1607
-		if (!isset($messageprops[$this->proptags['goid']])) {
1608
-			$this->setMeetingRequest($basedate);
1609
-		}
1610
-		else {
1611
-			$counter = $messageprops[$this->proptags['last_updatecounter']] + 1;
1612
-
1613
-			// increment value of last_updatecounter, last_updatecounter will be common for recurring series
1614
-			// so even if you sending an exception only you need to update the last_updatecounter in the recurring series message
1615
-			// this way we can make sure that every time we will be using a uniwue number for every operation
1616
-			mapi_setprops($this->message, [$this->proptags['last_updatecounter'] => $counter]);
1617
-		}
1618
-	}
1619
-
1620
-	/**
1621
-	 * Returns TRUE if we are the organiser of the meeting. Can be used with any type of meeting object.
1622
-	 */
1623
-	public function isLocalOrganiser() {
1624
-		$props = mapi_getprops($this->message, [$this->proptags['goid'], PR_MESSAGE_CLASS]);
1625
-
1626
-		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
1627
-			// we are checking with calendar item
1628
-			$calendarItem = $this->message;
1629
-		}
1630
-		else {
1631
-			// we are checking with meeting request / response / cancellation mail
1632
-			// get calendar items
1633
-			$calendarItem = $this->getCorrespondentCalendarItem(true);
1634
-		}
1635
-
1636
-		// even if we have received request/response for exception/occurrence then also
1637
-		// we can check recurring series for organizer, no need to check with exception/occurrence
1638
-
1639
-		if ($calendarItem !== false) {
1640
-			$messageProps = mapi_getprops($calendarItem, [$this->proptags['responsestatus']]);
1641
-
1642
-			if (isset($messageProps[$this->proptags['responsestatus']]) && $messageProps[$this->proptags['responsestatus']] === olResponseOrganized) {
1643
-				return true;
1644
-			}
1645
-		}
1646
-
1647
-		return false;
1648
-	}
1649
-
1650
-	/*
1478
+        if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] && !$basedate) {
1479
+            // Book resource
1480
+            $resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
1481
+
1482
+            if (!$this->errorSetResource) {
1483
+                $recurr = new Recurrence($this->openDefaultStore(), $this->message);
1484
+
1485
+                // First send meetingrequest for recurring item
1486
+                $this->submitMeetingRequest($this->message, $cancel, $prefix, false, $recurr, false, $modifiedRecips, $deletedRecips);
1487
+
1488
+                // Then send all meeting request for all exceptions
1489
+                $exceptions = $recurr->getAllExceptions();
1490
+                if ($exceptions) {
1491
+                    foreach ($exceptions as $exceptionBasedate) {
1492
+                        $attach = $recurr->getExceptionAttachment($exceptionBasedate);
1493
+
1494
+                        if ($attach) {
1495
+                            $occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1496
+                            $this->submitMeetingRequest($occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $modifiedRecips, $deletedRecips);
1497
+                            mapi_savechanges($attach);
1498
+                        }
1499
+                    }
1500
+                }
1501
+            }
1502
+        }
1503
+        else {
1504
+            // Basedate found, an exception is to be send
1505
+            if ($basedate) {
1506
+                $recurr = new Recurrence($this->openDefaultStore(), $this->message);
1507
+
1508
+                if ($cancel) {
1509
+                    // @TODO: remove occurrence from Resource's Calendar if resource was booked for whole series
1510
+                    $this->submitMeetingRequest($this->message, $cancel, $prefix, $basedate, $recurr, false);
1511
+                }
1512
+                else {
1513
+                    $attach = $recurr->getExceptionAttachment($basedate);
1514
+
1515
+                    if ($attach) {
1516
+                        $occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1517
+
1518
+                        // Book resource for this occurrence
1519
+                        $resourceRecipData = $this->bookResources($occurrenceItem, $cancel, $prefix, $basedate);
1520
+
1521
+                        if (!$this->errorSetResource) {
1522
+                            // Save all previous changes
1523
+                            mapi_savechanges($this->message);
1524
+
1525
+                            $this->submitMeetingRequest($occurrenceItem, $cancel, $prefix, $basedate, $recurr, true, $modifiedRecips, $deletedRecips);
1526
+                            mapi_savechanges($occurrenceItem);
1527
+                            mapi_savechanges($attach);
1528
+                        }
1529
+                    }
1530
+                }
1531
+            }
1532
+            else {
1533
+                // This is normal meeting
1534
+                $resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
1535
+
1536
+                if (!$this->errorSetResource) {
1537
+                    $this->submitMeetingRequest($this->message, $cancel, $prefix, false, false, false, $modifiedRecips, $deletedRecips);
1538
+                }
1539
+            }
1540
+        }
1541
+
1542
+        if (isset($this->errorSetResource) && $this->errorSetResource) {
1543
+            return [
1544
+                'error' => $this->errorSetResource,
1545
+                'displayname' => $this->recipientDisplayname,
1546
+            ];
1547
+        }
1548
+
1549
+        return true;
1550
+    }
1551
+
1552
+    /**
1553
+     * This function will get freebusy data for user based on the timeframe passed in arguments.
1554
+     *
1555
+     * @param {HexString} $entryID Entryid of the user for which we need to get freebusy data
1556
+     * @param {Number} $start start offset for freebusy publish range
1557
+     * @param {Number} $end end offset for freebusy publish range
1558
+     *
1559
+     * @return {Array} freebusy blocks for passed publish range
1560
+     */
1561
+    public function getFreeBusyInfo($entryID, $start, $end) {
1562
+        $result = [];
1563
+        $fbsupport = mapi_freebusysupport_open($this->session);
1564
+
1565
+        $fbDataArray = mapi_freebusysupport_loaddata($fbsupport, [$entryID]);
1566
+
1567
+        if ($fbDataArray[0] != null) {
1568
+            foreach ($fbDataArray as $fbDataUser) {
1569
+                $rangeuser1 = mapi_freebusydata_getpublishrange($fbDataUser);
1570
+                if ($rangeuser1 == null) {
1571
+                    return $result;
1572
+                }
1573
+
1574
+                $enumblock = mapi_freebusydata_enumblocks($fbDataUser, $start, $end);
1575
+                mapi_freebusyenumblock_reset($enumblock);
1576
+
1577
+                while (true) {
1578
+                    $blocks = mapi_freebusyenumblock_next($enumblock, 100);
1579
+                    if (!$blocks) {
1580
+                        break;
1581
+                    }
1582
+
1583
+                    foreach ($blocks as $blockItem) {
1584
+                        $result[] = $blockItem;
1585
+                    }
1586
+                }
1587
+            }
1588
+        }
1589
+
1590
+        mapi_freebusysupport_close($fbsupport);
1591
+
1592
+        return $result;
1593
+    }
1594
+
1595
+    /**
1596
+     * Updates the message after an update has been performed (for example,
1597
+     * changing the time of the meeting). This must be called before re-sending
1598
+     * the meeting request. You can also call this function instead of 'setMeetingRequest()'
1599
+     * as it will automatically call setMeetingRequest on this object if it is the first
1600
+     * call to this function.
1601
+     *
1602
+     * @param mixed $basedate
1603
+     */
1604
+    public function updateMeetingRequest($basedate = false) {
1605
+        $messageprops = mapi_getprops($this->message, [$this->proptags['last_updatecounter'], $this->proptags['goid']]);
1606
+
1607
+        if (!isset($messageprops[$this->proptags['goid']])) {
1608
+            $this->setMeetingRequest($basedate);
1609
+        }
1610
+        else {
1611
+            $counter = $messageprops[$this->proptags['last_updatecounter']] + 1;
1612
+
1613
+            // increment value of last_updatecounter, last_updatecounter will be common for recurring series
1614
+            // so even if you sending an exception only you need to update the last_updatecounter in the recurring series message
1615
+            // this way we can make sure that every time we will be using a uniwue number for every operation
1616
+            mapi_setprops($this->message, [$this->proptags['last_updatecounter'] => $counter]);
1617
+        }
1618
+    }
1619
+
1620
+    /**
1621
+     * Returns TRUE if we are the organiser of the meeting. Can be used with any type of meeting object.
1622
+     */
1623
+    public function isLocalOrganiser() {
1624
+        $props = mapi_getprops($this->message, [$this->proptags['goid'], PR_MESSAGE_CLASS]);
1625
+
1626
+        if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
1627
+            // we are checking with calendar item
1628
+            $calendarItem = $this->message;
1629
+        }
1630
+        else {
1631
+            // we are checking with meeting request / response / cancellation mail
1632
+            // get calendar items
1633
+            $calendarItem = $this->getCorrespondentCalendarItem(true);
1634
+        }
1635
+
1636
+        // even if we have received request/response for exception/occurrence then also
1637
+        // we can check recurring series for organizer, no need to check with exception/occurrence
1638
+
1639
+        if ($calendarItem !== false) {
1640
+            $messageProps = mapi_getprops($calendarItem, [$this->proptags['responsestatus']]);
1641
+
1642
+            if (isset($messageProps[$this->proptags['responsestatus']]) && $messageProps[$this->proptags['responsestatus']] === olResponseOrganized) {
1643
+                return true;
1644
+            }
1645
+        }
1646
+
1647
+        return false;
1648
+    }
1649
+
1650
+    /*
1651 1651
 	 * Support functions - INTERNAL ONLY
1652 1652
 	 ***************************************************************************************************
1653 1653
 	 */
1654 1654
 
1655
-	/**
1656
-	 * Return the tracking status of a recipient based on the IPM class (passed).
1657
-	 *
1658
-	 * @param mixed $class
1659
-	 */
1660
-	public function getTrackStatus($class) {
1661
-		$status = olRecipientTrackStatusNone;
1662
-
1663
-		switch ($class) {
1664
-			case 'IPM.Schedule.Meeting.Resp.Pos':
1665
-				$status = olRecipientTrackStatusAccepted;
1666
-
1667
-				break;
1668
-
1669
-			case 'IPM.Schedule.Meeting.Resp.Tent':
1670
-				$status = olRecipientTrackStatusTentative;
1671
-
1672
-				break;
1673
-
1674
-			case 'IPM.Schedule.Meeting.Resp.Neg':
1675
-				$status = olRecipientTrackStatusDeclined;
1676
-
1677
-				break;
1678
-		}
1679
-
1680
-		return $status;
1681
-	}
1682
-
1683
-	/**
1684
-	 * Function returns MAPIFolder resource of the folder that currently holds this meeting/meeting request
1685
-	 * object.
1686
-	 */
1687
-	public function openParentFolder() {
1688
-		$messageprops = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1689
-
1690
-		return mapi_msgstore_openentry($this->store, $messageprops[PR_PARENT_ENTRYID]);
1691
-	}
1692
-
1693
-	/**
1694
-	 * Function will return resource of the default calendar folder of store.
1695
-	 *
1696
-	 * @param MAPIStore $store {optional} user store whose default calendar should be opened
1697
-	 *
1698
-	 * @return MAPIFolder default calendar folder of store
1699
-	 */
1700
-	public function openDefaultCalendar($store = false) {
1701
-		return $this->openDefaultFolder(PR_IPM_APPOINTMENT_ENTRYID, $store);
1702
-	}
1703
-
1704
-	/**
1705
-	 * Function will return resource of the default outbox folder of store.
1706
-	 *
1707
-	 * @param MAPIStore $store {optional} user store whose default outbox should be opened
1708
-	 *
1709
-	 * @return MAPIFolder default outbox folder of store
1710
-	 */
1711
-	public function openDefaultOutbox($store = false) {
1712
-		return $this->openBaseFolder(PR_IPM_OUTBOX_ENTRYID, $store);
1713
-	}
1714
-
1715
-	/**
1716
-	 * Function will return resource of the default wastebasket folder of store.
1717
-	 *
1718
-	 * @param MAPIStore $store {optional} user store whose default wastebasket should be opened
1719
-	 *
1720
-	 * @return MAPIFolder default wastebasket folder of store
1721
-	 */
1722
-	public function openDefaultWastebasket($store = false) {
1723
-		return $this->openBaseFolder(PR_IPM_WASTEBASKET_ENTRYID, $store);
1724
-	}
1725
-
1726
-	/**
1727
-	 * Function will return resource of the default calendar folder of store.
1728
-	 *
1729
-	 * @param MAPIStore $store {optional} user store whose default calendar should be opened
1730
-	 *
1731
-	 * @return MAPIFolder default calendar folder of store
1732
-	 */
1733
-	public function getDefaultWastebasketEntryID($store = false) {
1734
-		return $this->getBaseEntryID(PR_IPM_WASTEBASKET_ENTRYID, $store);
1735
-	}
1736
-
1737
-	/**
1738
-	 * Function will return resource of the default sent mail folder of store.
1739
-	 *
1740
-	 * @param MAPIStore $store {optional} user store whose default sent mail should be opened
1741
-	 *
1742
-	 * @return MAPIFolder default sent mail folder of store
1743
-	 */
1744
-	public function getDefaultSentmailEntryID($store = false) {
1745
-		return $this->getBaseEntryID(PR_IPM_SENTMAIL_ENTRYID, $store);
1746
-	}
1747
-
1748
-	/**
1749
-	 * Function will return entryid of any default folder of store. This method is useful when you want
1750
-	 * to get entryid of folder which is stored as properties of inbox folder
1751
-	 * (PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID).
1752
-	 *
1753
-	 * @param PropTag   $prop  proptag of the folder for which we want to get entryid
1754
-	 * @param MAPIStore $store {optional} user store from which we need to get entryid of default folder
1755
-	 *
1756
-	 * @return BinString entryid of folder pointed by $prop
1757
-	 */
1758
-	public function getDefaultFolderEntryID($prop, $store = false) {
1759
-		try {
1760
-			$inbox = mapi_msgstore_getreceivefolder($store ? $store : $this->store);
1761
-			$inboxprops = mapi_getprops($inbox, [$prop]);
1762
-			if (isset($inboxprops[$prop])) {
1763
-				return $inboxprops[$prop];
1764
-			}
1765
-		}
1766
-		catch (MAPIException $e) {
1767
-			// public store doesn't support this method
1768
-			if ($e->getCode() == MAPI_E_NO_SUPPORT) {
1769
-				// don't propagate this error to parent handlers, if store doesn't support it
1770
-				$e->setHandled();
1771
-			}
1772
-		}
1773
-
1774
-		return false;
1775
-	}
1776
-
1777
-	/**
1778
-	 * Function will return resource of any default folder of store.
1779
-	 *
1780
-	 * @param PropTag   $prop  proptag of the folder that we want to open
1781
-	 * @param MAPIStore $store {optional} user store from which we need to open default folder
1782
-	 *
1783
-	 * @return MAPIFolder default folder of store
1784
-	 */
1785
-	public function openDefaultFolder($prop, $store = false) {
1786
-		$folder = false;
1787
-		$entryid = $this->getDefaultFolderEntryID($prop, $store);
1788
-
1789
-		if ($entryid !== false) {
1790
-			$folder = mapi_msgstore_openentry($store ? $store : $this->store, $entryid);
1791
-		}
1792
-
1793
-		return $folder;
1794
-	}
1795
-
1796
-	/**
1797
-	 * Function will return entryid of default folder from store. This method is useful when you want
1798
-	 * to get entryid of folder which is stored as store properties
1799
-	 * (PR_IPM_FAVORITES_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID).
1800
-	 *
1801
-	 * @param PropTag   $prop  proptag of the folder whose entryid we want to get
1802
-	 * @param MAPIStore $store {optional} user store from which we need to get entryid of default folder
1803
-	 *
1804
-	 * @return BinString entryid of default folder from store
1805
-	 */
1806
-	public function getBaseEntryID($prop, $store = false) {
1807
-		$storeprops = mapi_getprops($store ? $store : $this->store, [$prop]);
1808
-		if (!isset($storeprops[$prop])) {
1809
-			return false;
1810
-		}
1811
-
1812
-		return $storeprops[$prop];
1813
-	}
1814
-
1815
-	/**
1816
-	 * Function will return resource of any default folder of store.
1817
-	 *
1818
-	 * @param PropTag   $prop  proptag of the folder that we want to open
1819
-	 * @param MAPIStore $store {optional} user store from which we need to open default folder
1820
-	 *
1821
-	 * @return MAPIFolder default folder of store
1822
-	 */
1823
-	public function openBaseFolder($prop, $store = false) {
1824
-		$folder = false;
1825
-		$entryid = $this->getBaseEntryID($prop, $store);
1826
-
1827
-		if ($entryid !== false) {
1828
-			$folder = mapi_msgstore_openentry($store ? $store : $this->store, $entryid);
1829
-		}
1830
-
1831
-		return $folder;
1832
-	}
1833
-
1834
-	/**
1835
-	 * Function checks whether user has access over the specified folder or not.
1836
-	 *
1837
-	 * @param Binary    $entryid entryid The entryid of the folder to check
1838
-	 * @param MAPIStore $store   (optional) store from which folder should be opened
1839
-	 *
1840
-	 * @return bool true if user has an access over the folder, false if not
1841
-	 */
1842
-	public function checkFolderWriteAccess($entryid, $store = false) {
1843
-		$accessToFolder = false;
1844
-
1845
-		if (!empty($entryid)) {
1846
-			if ($store === false) {
1847
-				$store = $this->store;
1848
-			}
1849
-
1850
-			try {
1851
-				$folder = mapi_msgstore_openentry($store, $entryid);
1852
-				$folderProps = mapi_getprops($folder, [PR_ACCESS]);
1853
-				if (($folderProps[PR_ACCESS] & MAPI_ACCESS_CREATE_CONTENTS) === MAPI_ACCESS_CREATE_CONTENTS) {
1854
-					$accessToFolder = true;
1855
-				}
1856
-			}
1857
-			catch (MAPIException $e) {
1858
-				// we don't have rights to open folder, so return false
1859
-				if ($e->getCode() == MAPI_E_NO_ACCESS) {
1860
-					return $accessToFolder;
1861
-				}
1862
-
1863
-				// rethrow other errors
1864
-				throw $e;
1865
-			}
1866
-		}
1867
-
1868
-		return $accessToFolder;
1869
-	}
1870
-
1871
-	/**
1872
-	 * Function checks whether user has access over the specified folder or not.
1873
-	 *
1874
-	 * @param object MAPI Message Store Object
1875
-	 * @param mixed $store
1876
-	 *
1877
-	 * @return bool true if user has an access over the folder, false if not
1878
-	 */
1879
-	public function checkCalendarWriteAccess($store = false) {
1880
-		if ($store === false) {
1881
-			// If this meeting request is received by a delegate then open delegator's store.
1882
-			$messageProps = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID]);
1883
-			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
1884
-				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID]);
1885
-
1886
-				$store = $delegatorStore['store'];
1887
-			}
1888
-			else {
1889
-				$store = $this->store;
1890
-			}
1891
-		}
1892
-
1893
-		// If the store is a public folder, the calendar folder is the PARENT_ENTRYID of the calendar item
1894
-		$provider = mapi_getprops($store, [PR_MDB_PROVIDER]);
1895
-		if (isset($provider[PR_MDB_PROVIDER]) && $provider[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
1896
-			$entryid = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1897
-			$entryid = $entryid[PR_PARENT_ENTRYID];
1898
-		}
1899
-		else {
1900
-			$entryid = $this->getDefaultFolderEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1901
-			if ($entryid === false) {
1902
-				$entryid = $this->getBaseEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1903
-			}
1904
-
1905
-			if ($entryid === false) {
1906
-				return false;
1907
-			}
1908
-		}
1909
-
1910
-		return $this->checkFolderWriteAccess($entryid, $store);
1911
-	}
1912
-
1913
-	/**
1914
-	 * Function will resolve the user and open its store.
1915
-	 *
1916
-	 * @param string $ownerentryid the entryid of the user
1917
-	 *
1918
-	 * @return MAPIStore store of the user
1919
-	 */
1920
-	public function openCustomUserStore($ownerentryid) {
1921
-		$ab = mapi_openaddressbook($this->session);
1922
-
1923
-		try {
1924
-			$mailuser = mapi_ab_openentry($ab, $ownerentryid);
1925
-		}
1926
-		catch (MAPIException $e) {
1927
-			return;
1928
-		}
1929
-
1930
-		$mailuserprops = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]);
1931
-		$storeid = mapi_msgstore_createentryid($this->store, $mailuserprops[PR_EMAIL_ADDRESS]);
1932
-
1933
-		return mapi_openmsgstore($this->session, $storeid);
1934
-	}
1935
-
1936
-	/**
1937
-	 * Function which sends response to organizer when attendee accepts, declines or proposes new time to a received meeting request.
1938
-	 *
1939
-	 * @param int   $status              response status of attendee
1940
-	 * @param array $proposeNewTimeProps properties of attendee's proposal
1941
-	 * @param int   $basedate            date of occurrence which attendee has responded
1942
-	 * @param mixed $body
1943
-	 * @param mixed $store
1944
-	 * @param mixed $calFolder
1945
-	 */
1946
-	public function createResponse($status, $proposeNewTimeProps = [], $body = false, $store, $basedate = false, $calFolder) {
1947
-		$messageprops = mapi_getprops($this->message, [
1948
-			PR_SENT_REPRESENTING_ENTRYID,
1949
-			PR_SENT_REPRESENTING_EMAIL_ADDRESS,
1950
-			PR_SENT_REPRESENTING_ADDRTYPE,
1951
-			PR_SENT_REPRESENTING_NAME,
1952
-			PR_SENT_REPRESENTING_SEARCH_KEY,
1953
-			$this->proptags['goid'],
1954
-			$this->proptags['goid2'],
1955
-			$this->proptags['location'],
1956
-			$this->proptags['startdate'],
1957
-			$this->proptags['duedate'],
1958
-			$this->proptags['recurring'],
1959
-			$this->proptags['recurring_pattern'],
1960
-			$this->proptags['recurrence_data'],
1961
-			$this->proptags['timezone_data'],
1962
-			$this->proptags['timezone'],
1963
-			$this->proptags['updatecounter'],
1964
-			PR_SUBJECT,
1965
-			PR_MESSAGE_CLASS,
1966
-			PR_OWNER_APPT_ID,
1967
-			$this->proptags['is_exception'],
1968
-		]);
1969
-
1970
-		if ($basedate && !$this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
1971
-			// we are creating response from a recurring calendar item object
1972
-			// We found basedate,so opened occurrence and get properties.
1973
-			$recurr = new Recurrence($store, $this->message);
1974
-			$exception = $recurr->getExceptionAttachment($basedate);
1975
-
1976
-			if ($exception) {
1977
-				// Exception found, Now retrieve properties
1978
-				$imessage = mapi_attach_openobj($exception, 0);
1979
-				$imsgprops = mapi_getprops($imessage);
1980
-
1981
-				// If location is provided, copy it to the response
1982
-				if (isset($imsgprops[$this->proptags['location']])) {
1983
-					$messageprops[$this->proptags['location']] = $imsgprops[$this->proptags['location']];
1984
-				}
1985
-
1986
-				// Update $messageprops with timings of occurrence
1987
-				$messageprops[$this->proptags['startdate']] = $imsgprops[$this->proptags['startdate']];
1988
-				$messageprops[$this->proptags['duedate']] = $imsgprops[$this->proptags['duedate']];
1989
-
1990
-				// Meeting related properties
1991
-				$props[$this->proptags['meetingstatus']] = $imsgprops[$this->proptags['meetingstatus']];
1992
-				$props[$this->proptags['responsestatus']] = $imsgprops[$this->proptags['responsestatus']];
1993
-				$props[PR_SUBJECT] = $imsgprops[PR_SUBJECT];
1994
-			}
1995
-			else {
1996
-				// Exceptions is deleted.
1997
-				// Update $messageprops with timings of occurrence
1998
-				$messageprops[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
1999
-				$messageprops[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
2000
-
2001
-				$props[$this->proptags['meetingstatus']] = olNonMeeting;
2002
-				$props[$this->proptags['responsestatus']] = olResponseNone;
2003
-			}
2004
-
2005
-			$props[$this->proptags['recurring']] = false;
2006
-			$props[$this->proptags['is_exception']] = true;
2007
-		}
2008
-		else {
2009
-			// we are creating a response from meeting request mail (it could be recurring or non-recurring)
2010
-			// Send all recurrence info in response, if this is a recurrence meeting.
2011
-			$isRecurring = isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']];
2012
-			$isException = isset($messageprops[$this->proptags['is_exception']]) && $messageprops[$this->proptags['is_exception']];
2013
-			if ($isRecurring || $isException) {
2014
-				if ($isRecurring) {
2015
-					$props[$this->proptags['recurring']] = $messageprops[$this->proptags['recurring']];
2016
-				}
2017
-				if ($isException) {
2018
-					$props[$this->proptags['is_exception']] = $messageprops[$this->proptags['is_exception']];
2019
-				}
2020
-				$calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
2021
-
2022
-				$calendaritem = mapi_msgstore_openentry($store, $calendaritems[0]);
2023
-				$recurr = new Recurrence($store, $calendaritem);
2024
-			}
2025
-		}
2026
-
2027
-		// we are sending a response for recurring meeting request (or exception), so set some required properties
2028
-		if (isset($recurr) && $recurr) {
2029
-			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
2030
-				$props[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
2031
-			}
2032
-
2033
-			if (!empty($messageprops[$this->proptags['recurrence_data']])) {
2034
-				$props[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
2035
-			}
2036
-
2037
-			$props[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
2038
-			$props[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
2039
-
2040
-			$this->generateRecurDates($recurr, $messageprops, $props);
2041
-		}
2042
-
2043
-		// Create a response message
2044
-		$recip = [];
2045
-		$recip[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
2046
-		$recip[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2047
-		$recip[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
2048
-		$recip[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
2049
-		$recip[PR_RECIPIENT_TYPE] = MAPI_TO;
2050
-		$recip[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
2051
-
2052
-		switch ($status) {
2053
-			case olResponseAccepted:
2054
-				$classpostfix = 'Pos';
2055
-				$subjectprefix = _('Accepted');
2056
-
2057
-				break;
2058
-
2059
-			case olResponseDeclined:
2060
-				$classpostfix = 'Neg';
2061
-				$subjectprefix = _('Declined');
2062
-
2063
-				break;
2064
-
2065
-			case olResponseTentative:
2066
-				$classpostfix = 'Tent';
2067
-				$subjectprefix = _('Tentatively accepted');
2068
-
2069
-				break;
2070
-		}
2071
-
2072
-		if (!empty($proposeNewTimeProps)) {
2073
-			// if attendee has proposed new time then change subject prefix
2074
-			$subjectprefix = _('New Time Proposed');
2075
-		}
2076
-
2077
-		$props[PR_SUBJECT] = $subjectprefix . ': ' . $messageprops[PR_SUBJECT];
2078
-
2079
-		$props[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Resp.' . $classpostfix;
2080
-		if (isset($messageprops[PR_OWNER_APPT_ID])) {
2081
-			$props[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
2082
-		}
2083
-
2084
-		// Set GlobalId AND CleanGlobalId, if exception then also set basedate into GlobalId(0x3).
2085
-		$props[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
2086
-		$props[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
2087
-		$props[$this->proptags['updatecounter']] = isset($messageprops[$this->proptags['updatecounter']]) ? $messageprops[$this->proptags['updatecounter']] : 0;
2088
-
2089
-		if (!empty($proposeNewTimeProps)) {
2090
-			// merge proposal properties to message properties which will be sent to organizer
2091
-			$props = $proposeNewTimeProps + $props;
2092
-		}
2093
-
2094
-		// Set body message in Appointment
2095
-		if (isset($body)) {
2096
-			$props[PR_BODY] = $this->getMeetingTimeInfo() ? $this->getMeetingTimeInfo() : $body;
2097
-		}
2098
-
2099
-		// PR_START_DATE/PR_END_DATE is used in the UI in Outlook on the response message
2100
-		$props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
2101
-		$props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
2102
-
2103
-		// Set startdate and duedate in response mail.
2104
-		$props[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
2105
-		$props[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
2106
-
2107
-		// responselocation is used in the UI in Outlook on the response message
2108
-		if (isset($messageprops[$this->proptags['location']])) {
2109
-			$props[$this->proptags['responselocation']] = $messageprops[$this->proptags['location']];
2110
-			$props[$this->proptags['location']] = $messageprops[$this->proptags['location']];
2111
-		}
2112
-
2113
-		$message = $this->createOutgoingMessage($store);
2114
-
2115
-		mapi_setprops($message, $props);
2116
-		mapi_message_modifyrecipients($message, MODRECIP_ADD, [$recip]);
2117
-		mapi_savechanges($message);
2118
-		mapi_message_submitmessage($message);
2119
-	}
2120
-
2121
-	/**
2122
-	 * Function which finds items in calendar based on globalId and cleanGlobalId.
2123
-	 *
2124
-	 * @param binary     $goid             GlobalID(0x3) of item
2125
-	 * @param MAPIFolder $calendar         MAPI_folder of user (optional)
2126
-	 * @param bool       $useCleanGlobalId if true then search should be performed on cleanGlobalId(0x23) else globalId(0x3)
2127
-	 */
2128
-	public function findCalendarItems($goid, $calendar = false, $useCleanGlobalId = false) {
2129
-		if ($calendar === false) {
2130
-			// Open the Calendar
2131
-			$calendar = $this->openDefaultCalendar();
2132
-		}
2133
-
2134
-		// Find the item by restricting all items to the correct ID
2135
-		$restrict = [
2136
-			RES_AND,
2137
-			[
2138
-				[
2139
-					RES_PROPERTY,
2140
-					[
2141
-						RELOP => RELOP_EQ,
2142
-						ULPROPTAG => ($useCleanGlobalId === true ? $this->proptags['goid2'] : $this->proptags['goid']),
2143
-						VALUE => $goid,
2144
-					],
2145
-				],
2146
-			], ];
2147
-
2148
-		$calendarcontents = mapi_folder_getcontentstable($calendar);
2149
-
2150
-		$rows = mapi_table_queryallrows($calendarcontents, [PR_ENTRYID], $restrict);
2151
-
2152
-		if (empty($rows)) {
2153
-			return;
2154
-		}
2155
-
2156
-		$calendaritems = [];
2157
-
2158
-		// In principle, there should only be one row, but we'll handle them all just in case
2159
-		foreach ($rows as $row) {
2160
-			$calendaritems[] = $row[PR_ENTRYID];
2161
-		}
2162
-
2163
-		return $calendaritems;
2164
-	}
2165
-
2166
-	// Returns TRUE if both entryid's are equal. Equality is defined by both entryid's pointing at the
2167
-	// same SMTP address when converted to SMTP
2168
-	public function compareABEntryIDs($entryid1, $entryid2) {
2169
-		// If the session was not passed, just do a 'normal' compare.
2170
-		if (!$this->session) {
2171
-			return $entryid1 == $entryid2;
2172
-		}
2173
-
2174
-		$smtp1 = $this->getSMTPAddress($entryid1);
2175
-		$smtp2 = $this->getSMTPAddress($entryid2);
2176
-
2177
-		if ($smtp1 == $smtp2) {
2178
-			return true;
2179
-		}
2180
-
2181
-		return false;
2182
-	}
2183
-
2184
-	// Gets the SMTP address of the passed addressbook entryid
2185
-	public function getSMTPAddress($entryid) {
2186
-		if (!$this->session) {
2187
-			return false;
2188
-		}
2189
-
2190
-		$ab = mapi_openaddressbook($this->session);
2191
-
2192
-		$abitem = mapi_ab_openentry($ab, $entryid);
2193
-
2194
-		if (!$abitem) {
2195
-			return '';
2196
-		}
2197
-
2198
-		$props = mapi_getprops($abitem, [PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2199
-
2200
-		if ($props[PR_ADDRTYPE] == 'SMTP') {
2201
-			return $props[PR_EMAIL_ADDRESS];
2202
-		}
2203
-
2204
-		return $props[PR_SMTP_ADDRESS];
2205
-	}
2206
-
2207
-	/**
2208
-	 * Gets the properties associated with the owner of the passed store:
2209
-	 * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY.
2210
-	 *
2211
-	 * @param $store message store
2212
-	 * @param $fallbackToLoggedInUser if true then return properties of logged in user instead of mailbox owner
2213
-	 * not used when passed store is public store. for public store we are always returning logged in user's info.
2214
-	 *
2215
-	 * @return properties of logged in user in an array in sequence of display_name, email address, address type,
2216
-	 *                    entryid and search key
2217
-	 */
2218
-	public function getOwnerAddress($store, $fallbackToLoggedInUser = true) {
2219
-		if (!$this->session) {
2220
-			return false;
2221
-		}
2222
-
2223
-		$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID]);
2224
-
2225
-		$ownerEntryId = false;
2226
-		if (isset($storeProps[PR_USER_ENTRYID]) && $storeProps[PR_USER_ENTRYID]) {
2227
-			$ownerEntryId = $storeProps[PR_USER_ENTRYID];
2228
-		}
2229
-
2230
-		if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID]) && $storeProps[PR_MAILBOX_OWNER_ENTRYID] && !$fallbackToLoggedInUser) {
2231
-			$ownerEntryId = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
2232
-		}
2233
-
2234
-		if ($ownerEntryId) {
2235
-			$ab = mapi_openaddressbook($this->session);
2236
-
2237
-			$zarafaUser = mapi_ab_openentry($ab, $ownerEntryId);
2238
-			if (!$zarafaUser) {
2239
-				return false;
2240
-			}
2241
-
2242
-			$ownerProps = mapi_getprops($zarafaUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2243
-
2244
-			$addrType = $ownerProps[PR_ADDRTYPE];
2245
-			$name = $ownerProps[PR_DISPLAY_NAME];
2246
-			$emailAddr = $ownerProps[PR_EMAIL_ADDRESS];
2247
-			$searchKey = $ownerProps[PR_SEARCH_KEY];
2248
-			$entryId = $ownerEntryId;
2249
-
2250
-			return [$name, $emailAddr, $addrType, $entryId, $searchKey];
2251
-		}
2252
-
2253
-		return false;
2254
-	}
2255
-
2256
-	// Opens this session's default message store
2257
-	public function openDefaultStore() {
2258
-		$storestable = mapi_getmsgstorestable($this->session);
2259
-		$rows = mapi_table_queryallrows($storestable, [PR_ENTRYID, PR_DEFAULT_STORE]);
2260
-
2261
-		foreach ($rows as $row) {
2262
-			if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) {
2263
-				$entryid = $row[PR_ENTRYID];
2264
-
2265
-				break;
2266
-			}
2267
-		}
2268
-
2269
-		if (!$entryid) {
2270
-			return false;
2271
-		}
2272
-
2273
-		return mapi_openmsgstore($this->session, $entryid);
2274
-	}
2275
-
2276
-	/**
2277
-	 *  Function which adds organizer to recipient list which is passed.
2278
-	 *  This function also checks if it has organizer.
2279
-	 *
2280
-	 * @param array $messageProps message properties
2281
-	 * @param array $recipients   recipients list of message
2282
-	 * @param bool  $isException  true if we are processing recipient of exception
2283
-	 */
2284
-	public function addOrganizer($messageProps, &$recipients, $isException = false) {
2285
-		$hasOrganizer = false;
2286
-		// Check if meeting already has an organizer.
2287
-		foreach ($recipients as $key => $recipient) {
2288
-			if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
2289
-				$hasOrganizer = true;
2290
-			}
2291
-			elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
2292
-				// Recipients for an occurrence
2293
-				$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
2294
-			}
2295
-		}
2296
-
2297
-		if (!$hasOrganizer) {
2298
-			// Create organizer.
2299
-			$organizer = [];
2300
-			$organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
2301
-			$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2302
-			$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2303
-			$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
2304
-			$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2305
-			$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
2306
-			$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
2307
-			$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
2308
-			$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
2309
-
2310
-			// Add organizer to recipients list.
2311
-			array_unshift($recipients, $organizer);
2312
-		}
2313
-	}
2314
-
2315
-	/**
2316
-	 * Function which removes an exception/occurrence from recurrencing meeting
2317
-	 * when a meeting cancellation of an occurrence is processed.
2318
-	 *
2319
-	 * @param string   $basedate basedate of an occurrence
2320
-	 * @param resource $message  recurring item from which occurrence has to be deleted
2321
-	 * @param resource $store    MAPI_MSG_Store which contains the item
2322
-	 */
2323
-	public function doRemoveExceptionFromCalendar($basedate, $message, $store) {
2324
-		$recurr = new Recurrence($store, $message);
2325
-		$recurr->createException([], $basedate, true);
2326
-		mapi_savechanges($message);
2327
-	}
2328
-
2329
-	/**
2330
-	 * Function which returns basedate of an changed occurrence from globalID of meeting request.
2331
-	 *
2332
-	 *@param binary $goid globalID
2333
-	 *
2334
-	 *@return bool true if basedate is found else false it not found
2335
-	 */
2336
-	public function getBasedateFromGlobalID($goid) {
2337
-		$hexguid = bin2hex($goid);
2338
-		$hexbase = substr($hexguid, 32, 8);
2339
-		$day = hexdec(substr($hexbase, 6, 2));
2340
-		$month = hexdec(substr($hexbase, 4, 2));
2341
-		$year = hexdec(substr($hexbase, 0, 4));
2342
-
2343
-		if ($day && $month && $year) {
2344
-			return gmmktime(0, 0, 0, $month, $day, $year);
2345
-		}
2346
-
2347
-		return false;
2348
-	}
2349
-
2350
-	/**
2351
-	 * Function which sets basedate in globalID of changed occurrence which is to be send.
2352
-	 *
2353
-	 *@param binary $goid globalID
2354
-	 *@param string basedate of changed occurrence
2355
-	 * @param mixed $basedate
2356
-	 *
2357
-	 *@return binary globalID with basedate in it
2358
-	 */
2359
-	public function setBasedateInGlobalID($goid, $basedate = false) {
2360
-		$hexguid = bin2hex($goid);
2361
-		$year = $basedate ? sprintf('%04s', dechex(gmdate('Y', $basedate))) : '0000';
2362
-		$month = $basedate ? sprintf('%02s', dechex(gmdate('m', $basedate))) : '00';
2363
-		$day = $basedate ? sprintf('%02s', dechex(gmdate('d', $basedate))) : '00';
2364
-
2365
-		return hex2bin(strtoupper(substr($hexguid, 0, 32) . $year . $month . $day . substr($hexguid, 40)));
2366
-	}
2367
-
2368
-	/**
2369
-	 * Function which replaces attachments with copy_from in copy_to.
2370
-	 *
2371
-	 * @param MAPIMessage $copy_from      MAPI_message from which attachments are to be copied
2372
-	 * @param MAPIMessage $copy_to        MAPI_message to which attachment are to be copied
2373
-	 * @param bool        $copyExceptions if true then all exceptions should also be sent as attachments
2374
-	 * @param mixed       $copyFrom
2375
-	 * @param mixed       $copyTo
2376
-	 */
2377
-	public function replaceAttachments($copyFrom, $copyTo, $copyExceptions = true) {
2378
-		/* remove all old attachments */
2379
-		$attachmentTable = mapi_message_getattachmenttable($copyTo);
2380
-		if ($attachmentTable) {
2381
-			$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2382
-
2383
-			foreach ($attachments as $attachProps) {
2384
-				/* remove exceptions too? */
2385
-				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2386
-					continue;
2387
-				}
2388
-				mapi_message_deleteattach($copyTo, $attachProps[PR_ATTACH_NUM]);
2389
-			}
2390
-		}
2391
-		$attachmentTable = false;
2392
-
2393
-		/* copy new attachments */
2394
-		$attachmentTable = mapi_message_getattachmenttable($copyFrom);
2395
-		if ($attachmentTable) {
2396
-			$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2397
-
2398
-			foreach ($attachments as $attachProps) {
2399
-				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2400
-					continue;
2401
-				}
2402
-
2403
-				$attachOld = mapi_message_openattach($copyFrom, (int) $attachProps[PR_ATTACH_NUM]);
2404
-				$attachNewResourceMsg = mapi_message_createattach($copyTo);
2405
-				mapi_copyto($attachOld, [], [], $attachNewResourceMsg, 0);
2406
-				mapi_savechanges($attachNewResourceMsg);
2407
-			}
2408
-		}
2409
-	}
2410
-
2411
-	/**
2412
-	 * Function which replaces recipients in copy_to with recipients from copyFrom.
2413
-	 *
2414
-	 * @param MAPIMessage $copyFrom   MAPI_message from which recipients are to be copied
2415
-	 * @param MAPIMessage $copyTo     MAPI_message to which recipients are to be copied
2416
-	 * @param bool        $isDelegate indicates delegate is processing
2417
-	 *                                so don't copy delegate information to recipient table
2418
-	 */
2419
-	public function replaceRecipients($copyFrom, $copyTo, $isDelegate = false) {
2420
-		$recipientTable = mapi_message_getrecipienttable($copyFrom);
2421
-
2422
-		// If delegate, then do not add the delegate in recipients
2423
-		if ($isDelegate) {
2424
-			$delegate = mapi_getprops($copyFrom, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2425
-			$res = [RES_PROPERTY, [
2426
-				RELOP => RELOP_NE,
2427
-				ULPROPTAG => PR_EMAIL_ADDRESS,
2428
-				VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2429
-			],
2430
-			];
2431
-			$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops, $res);
2432
-		}
2433
-		else {
2434
-			$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops);
2435
-		}
2436
-
2437
-		$copyToRecipientTable = mapi_message_getrecipienttable($copyTo);
2438
-		$copyToRecipientRows = mapi_table_queryallrows($copyToRecipientTable, [PR_ROWID]);
2439
-
2440
-		mapi_message_modifyrecipients($copyTo, MODRECIP_REMOVE, $copyToRecipientRows);
2441
-		mapi_message_modifyrecipients($copyTo, MODRECIP_ADD, $recipients);
2442
-	}
2443
-
2444
-	/**
2445
-	 * Function creates meeting item in resource's calendar.
2446
-	 *
2447
-	 * @param resource $message  MAPI_message which is to create in resource's calendar
2448
-	 * @param bool     $cancel   cancel meeting
2449
-	 * @param string   $prefix   prefix for subject of meeting
2450
-	 * @param mixed    $basedate
2451
-	 */
2452
-	public function bookResources($message, $cancel, $prefix, $basedate = false) {
2453
-		if (!$this->enableDirectBooking) {
2454
-			return [];
2455
-		}
2456
-
2457
-		// Get the properties of the message
2458
-		$messageprops = mapi_getprops($message);
2459
-
2460
-		if ($basedate) {
2461
-			$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID]);
2462
-
2463
-			$messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedate);
2464
-			$messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
2465
-
2466
-			// Delete properties which are not needed.
2467
-			$deleteProps = [$this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD];
2468
-			foreach ($deleteProps as $propID) {
2469
-				if (isset($messageprops[$propID])) {
2470
-					unset($messageprops[$propID]);
2471
-				}
2472
-			}
2473
-
2474
-			if (isset($messageprops[$this->proptags['recurring']])) {
2475
-				$messageprops[$this->proptags['recurring']] = false;
2476
-			}
2477
-
2478
-			// Set Outlook properties
2479
-			$messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']];
2480
-			$messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']];
2481
-			$messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']];
2482
-			$messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']];
2483
-			$messageprops[$this->proptags['attendee_critical_change']] = time();
2484
-			$messageprops[$this->proptags['owner_critical_change']] = time();
2485
-		}
2486
-
2487
-		// Get resource recipients
2488
-		$getResourcesRestriction = [
2489
-			RES_AND,
2490
-			[
2491
-				[
2492
-					RES_PROPERTY,
2493
-					[
2494
-						RELOP => RELOP_EQ,    // Equals recipient type 3: Resource
2495
-						ULPROPTAG => PR_RECIPIENT_TYPE,
2496
-						VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2497
-					],
2498
-				], ],
2499
-		];
2500
-		$recipienttable = mapi_message_getrecipienttable($message);
2501
-		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2502
-
2503
-		$this->errorSetResource = false;
2504
-		$resourceRecipData = [];
2505
-
2506
-		// Put appointment into store resource users
2507
-		$i = 0;
2508
-		$len = count($resourceRecipients);
2509
-		while (!$this->errorSetResource && $i < $len) {
2510
-			$userStore = $this->openCustomUserStore($resourceRecipients[$i][PR_ENTRYID]);
2511
-
2512
-			// Open root folder
2513
-			$userRoot = mapi_msgstore_openentry($userStore, null);
2514
-
2515
-			// Get calendar entryID
2516
-			$userRootProps = mapi_getprops($userRoot, [PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS]);
2517
-
2518
-			// Open Calendar folder
2519
-			$accessToFolder = false;
2520
-
2521
-			try {
2522
-				// @FIXME this checks delegate has access to resource's calendar folder
2523
-				// but it should use boss' credentials
2524
-
2525
-				$accessToFolder = $this->checkCalendarWriteAccess($this->store);
2526
-				if ($accessToFolder) {
2527
-					$calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
2528
-				}
2529
-			}
2530
-			catch (MAPIException $e) {
2531
-				$e->setHandled();
2532
-				$this->errorSetResource = 1; // No access
2533
-			}
2534
-
2535
-			if ($accessToFolder) {
2536
-				/**
2537
-				 * Get the LocalFreebusy message that contains the properties that
2538
-				 * are set to accept or decline resource meeting requests.
2539
-				 */
2540
-				// Use PR_FREEBUSY_ENTRYIDS[1] to open folder the LocalFreeBusy msg
2541
-				$localFreebusyMsg = mapi_msgstore_openentry($userStore, $userRootProps[PR_FREEBUSY_ENTRYIDS][1]);
2542
-				if ($localFreebusyMsg) {
2543
-					$props = mapi_getprops($localFreebusyMsg, [PR_PROCESS_MEETING_REQUESTS, PR_DECLINE_RECURRING_MEETING_REQUESTS, PR_DECLINE_CONFLICTING_MEETING_REQUESTS]);
2544
-
2545
-					$acceptMeetingRequests = isset($props[PR_PROCESS_MEETING_REQUESTS]) ? $props[PR_PROCESS_MEETING_REQUESTS] : false;
2546
-					$declineRecurringMeetingRequests = isset($props[PR_DECLINE_RECURRING_MEETING_REQUESTS]) ? $props[PR_DECLINE_RECURRING_MEETING_REQUESTS] : false;
2547
-					$declineConflictingMeetingRequests = isset($props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS]) ? $props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS] : false;
2548
-
2549
-					if (!$acceptMeetingRequests) {
2550
-						/*
1655
+    /**
1656
+     * Return the tracking status of a recipient based on the IPM class (passed).
1657
+     *
1658
+     * @param mixed $class
1659
+     */
1660
+    public function getTrackStatus($class) {
1661
+        $status = olRecipientTrackStatusNone;
1662
+
1663
+        switch ($class) {
1664
+            case 'IPM.Schedule.Meeting.Resp.Pos':
1665
+                $status = olRecipientTrackStatusAccepted;
1666
+
1667
+                break;
1668
+
1669
+            case 'IPM.Schedule.Meeting.Resp.Tent':
1670
+                $status = olRecipientTrackStatusTentative;
1671
+
1672
+                break;
1673
+
1674
+            case 'IPM.Schedule.Meeting.Resp.Neg':
1675
+                $status = olRecipientTrackStatusDeclined;
1676
+
1677
+                break;
1678
+        }
1679
+
1680
+        return $status;
1681
+    }
1682
+
1683
+    /**
1684
+     * Function returns MAPIFolder resource of the folder that currently holds this meeting/meeting request
1685
+     * object.
1686
+     */
1687
+    public function openParentFolder() {
1688
+        $messageprops = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1689
+
1690
+        return mapi_msgstore_openentry($this->store, $messageprops[PR_PARENT_ENTRYID]);
1691
+    }
1692
+
1693
+    /**
1694
+     * Function will return resource of the default calendar folder of store.
1695
+     *
1696
+     * @param MAPIStore $store {optional} user store whose default calendar should be opened
1697
+     *
1698
+     * @return MAPIFolder default calendar folder of store
1699
+     */
1700
+    public function openDefaultCalendar($store = false) {
1701
+        return $this->openDefaultFolder(PR_IPM_APPOINTMENT_ENTRYID, $store);
1702
+    }
1703
+
1704
+    /**
1705
+     * Function will return resource of the default outbox folder of store.
1706
+     *
1707
+     * @param MAPIStore $store {optional} user store whose default outbox should be opened
1708
+     *
1709
+     * @return MAPIFolder default outbox folder of store
1710
+     */
1711
+    public function openDefaultOutbox($store = false) {
1712
+        return $this->openBaseFolder(PR_IPM_OUTBOX_ENTRYID, $store);
1713
+    }
1714
+
1715
+    /**
1716
+     * Function will return resource of the default wastebasket folder of store.
1717
+     *
1718
+     * @param MAPIStore $store {optional} user store whose default wastebasket should be opened
1719
+     *
1720
+     * @return MAPIFolder default wastebasket folder of store
1721
+     */
1722
+    public function openDefaultWastebasket($store = false) {
1723
+        return $this->openBaseFolder(PR_IPM_WASTEBASKET_ENTRYID, $store);
1724
+    }
1725
+
1726
+    /**
1727
+     * Function will return resource of the default calendar folder of store.
1728
+     *
1729
+     * @param MAPIStore $store {optional} user store whose default calendar should be opened
1730
+     *
1731
+     * @return MAPIFolder default calendar folder of store
1732
+     */
1733
+    public function getDefaultWastebasketEntryID($store = false) {
1734
+        return $this->getBaseEntryID(PR_IPM_WASTEBASKET_ENTRYID, $store);
1735
+    }
1736
+
1737
+    /**
1738
+     * Function will return resource of the default sent mail folder of store.
1739
+     *
1740
+     * @param MAPIStore $store {optional} user store whose default sent mail should be opened
1741
+     *
1742
+     * @return MAPIFolder default sent mail folder of store
1743
+     */
1744
+    public function getDefaultSentmailEntryID($store = false) {
1745
+        return $this->getBaseEntryID(PR_IPM_SENTMAIL_ENTRYID, $store);
1746
+    }
1747
+
1748
+    /**
1749
+     * Function will return entryid of any default folder of store. This method is useful when you want
1750
+     * to get entryid of folder which is stored as properties of inbox folder
1751
+     * (PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID).
1752
+     *
1753
+     * @param PropTag   $prop  proptag of the folder for which we want to get entryid
1754
+     * @param MAPIStore $store {optional} user store from which we need to get entryid of default folder
1755
+     *
1756
+     * @return BinString entryid of folder pointed by $prop
1757
+     */
1758
+    public function getDefaultFolderEntryID($prop, $store = false) {
1759
+        try {
1760
+            $inbox = mapi_msgstore_getreceivefolder($store ? $store : $this->store);
1761
+            $inboxprops = mapi_getprops($inbox, [$prop]);
1762
+            if (isset($inboxprops[$prop])) {
1763
+                return $inboxprops[$prop];
1764
+            }
1765
+        }
1766
+        catch (MAPIException $e) {
1767
+            // public store doesn't support this method
1768
+            if ($e->getCode() == MAPI_E_NO_SUPPORT) {
1769
+                // don't propagate this error to parent handlers, if store doesn't support it
1770
+                $e->setHandled();
1771
+            }
1772
+        }
1773
+
1774
+        return false;
1775
+    }
1776
+
1777
+    /**
1778
+     * Function will return resource of any default folder of store.
1779
+     *
1780
+     * @param PropTag   $prop  proptag of the folder that we want to open
1781
+     * @param MAPIStore $store {optional} user store from which we need to open default folder
1782
+     *
1783
+     * @return MAPIFolder default folder of store
1784
+     */
1785
+    public function openDefaultFolder($prop, $store = false) {
1786
+        $folder = false;
1787
+        $entryid = $this->getDefaultFolderEntryID($prop, $store);
1788
+
1789
+        if ($entryid !== false) {
1790
+            $folder = mapi_msgstore_openentry($store ? $store : $this->store, $entryid);
1791
+        }
1792
+
1793
+        return $folder;
1794
+    }
1795
+
1796
+    /**
1797
+     * Function will return entryid of default folder from store. This method is useful when you want
1798
+     * to get entryid of folder which is stored as store properties
1799
+     * (PR_IPM_FAVORITES_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID).
1800
+     *
1801
+     * @param PropTag   $prop  proptag of the folder whose entryid we want to get
1802
+     * @param MAPIStore $store {optional} user store from which we need to get entryid of default folder
1803
+     *
1804
+     * @return BinString entryid of default folder from store
1805
+     */
1806
+    public function getBaseEntryID($prop, $store = false) {
1807
+        $storeprops = mapi_getprops($store ? $store : $this->store, [$prop]);
1808
+        if (!isset($storeprops[$prop])) {
1809
+            return false;
1810
+        }
1811
+
1812
+        return $storeprops[$prop];
1813
+    }
1814
+
1815
+    /**
1816
+     * Function will return resource of any default folder of store.
1817
+     *
1818
+     * @param PropTag   $prop  proptag of the folder that we want to open
1819
+     * @param MAPIStore $store {optional} user store from which we need to open default folder
1820
+     *
1821
+     * @return MAPIFolder default folder of store
1822
+     */
1823
+    public function openBaseFolder($prop, $store = false) {
1824
+        $folder = false;
1825
+        $entryid = $this->getBaseEntryID($prop, $store);
1826
+
1827
+        if ($entryid !== false) {
1828
+            $folder = mapi_msgstore_openentry($store ? $store : $this->store, $entryid);
1829
+        }
1830
+
1831
+        return $folder;
1832
+    }
1833
+
1834
+    /**
1835
+     * Function checks whether user has access over the specified folder or not.
1836
+     *
1837
+     * @param Binary    $entryid entryid The entryid of the folder to check
1838
+     * @param MAPIStore $store   (optional) store from which folder should be opened
1839
+     *
1840
+     * @return bool true if user has an access over the folder, false if not
1841
+     */
1842
+    public function checkFolderWriteAccess($entryid, $store = false) {
1843
+        $accessToFolder = false;
1844
+
1845
+        if (!empty($entryid)) {
1846
+            if ($store === false) {
1847
+                $store = $this->store;
1848
+            }
1849
+
1850
+            try {
1851
+                $folder = mapi_msgstore_openentry($store, $entryid);
1852
+                $folderProps = mapi_getprops($folder, [PR_ACCESS]);
1853
+                if (($folderProps[PR_ACCESS] & MAPI_ACCESS_CREATE_CONTENTS) === MAPI_ACCESS_CREATE_CONTENTS) {
1854
+                    $accessToFolder = true;
1855
+                }
1856
+            }
1857
+            catch (MAPIException $e) {
1858
+                // we don't have rights to open folder, so return false
1859
+                if ($e->getCode() == MAPI_E_NO_ACCESS) {
1860
+                    return $accessToFolder;
1861
+                }
1862
+
1863
+                // rethrow other errors
1864
+                throw $e;
1865
+            }
1866
+        }
1867
+
1868
+        return $accessToFolder;
1869
+    }
1870
+
1871
+    /**
1872
+     * Function checks whether user has access over the specified folder or not.
1873
+     *
1874
+     * @param object MAPI Message Store Object
1875
+     * @param mixed $store
1876
+     *
1877
+     * @return bool true if user has an access over the folder, false if not
1878
+     */
1879
+    public function checkCalendarWriteAccess($store = false) {
1880
+        if ($store === false) {
1881
+            // If this meeting request is received by a delegate then open delegator's store.
1882
+            $messageProps = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID]);
1883
+            if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
1884
+                $delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID]);
1885
+
1886
+                $store = $delegatorStore['store'];
1887
+            }
1888
+            else {
1889
+                $store = $this->store;
1890
+            }
1891
+        }
1892
+
1893
+        // If the store is a public folder, the calendar folder is the PARENT_ENTRYID of the calendar item
1894
+        $provider = mapi_getprops($store, [PR_MDB_PROVIDER]);
1895
+        if (isset($provider[PR_MDB_PROVIDER]) && $provider[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
1896
+            $entryid = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1897
+            $entryid = $entryid[PR_PARENT_ENTRYID];
1898
+        }
1899
+        else {
1900
+            $entryid = $this->getDefaultFolderEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1901
+            if ($entryid === false) {
1902
+                $entryid = $this->getBaseEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1903
+            }
1904
+
1905
+            if ($entryid === false) {
1906
+                return false;
1907
+            }
1908
+        }
1909
+
1910
+        return $this->checkFolderWriteAccess($entryid, $store);
1911
+    }
1912
+
1913
+    /**
1914
+     * Function will resolve the user and open its store.
1915
+     *
1916
+     * @param string $ownerentryid the entryid of the user
1917
+     *
1918
+     * @return MAPIStore store of the user
1919
+     */
1920
+    public function openCustomUserStore($ownerentryid) {
1921
+        $ab = mapi_openaddressbook($this->session);
1922
+
1923
+        try {
1924
+            $mailuser = mapi_ab_openentry($ab, $ownerentryid);
1925
+        }
1926
+        catch (MAPIException $e) {
1927
+            return;
1928
+        }
1929
+
1930
+        $mailuserprops = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]);
1931
+        $storeid = mapi_msgstore_createentryid($this->store, $mailuserprops[PR_EMAIL_ADDRESS]);
1932
+
1933
+        return mapi_openmsgstore($this->session, $storeid);
1934
+    }
1935
+
1936
+    /**
1937
+     * Function which sends response to organizer when attendee accepts, declines or proposes new time to a received meeting request.
1938
+     *
1939
+     * @param int   $status              response status of attendee
1940
+     * @param array $proposeNewTimeProps properties of attendee's proposal
1941
+     * @param int   $basedate            date of occurrence which attendee has responded
1942
+     * @param mixed $body
1943
+     * @param mixed $store
1944
+     * @param mixed $calFolder
1945
+     */
1946
+    public function createResponse($status, $proposeNewTimeProps = [], $body = false, $store, $basedate = false, $calFolder) {
1947
+        $messageprops = mapi_getprops($this->message, [
1948
+            PR_SENT_REPRESENTING_ENTRYID,
1949
+            PR_SENT_REPRESENTING_EMAIL_ADDRESS,
1950
+            PR_SENT_REPRESENTING_ADDRTYPE,
1951
+            PR_SENT_REPRESENTING_NAME,
1952
+            PR_SENT_REPRESENTING_SEARCH_KEY,
1953
+            $this->proptags['goid'],
1954
+            $this->proptags['goid2'],
1955
+            $this->proptags['location'],
1956
+            $this->proptags['startdate'],
1957
+            $this->proptags['duedate'],
1958
+            $this->proptags['recurring'],
1959
+            $this->proptags['recurring_pattern'],
1960
+            $this->proptags['recurrence_data'],
1961
+            $this->proptags['timezone_data'],
1962
+            $this->proptags['timezone'],
1963
+            $this->proptags['updatecounter'],
1964
+            PR_SUBJECT,
1965
+            PR_MESSAGE_CLASS,
1966
+            PR_OWNER_APPT_ID,
1967
+            $this->proptags['is_exception'],
1968
+        ]);
1969
+
1970
+        if ($basedate && !$this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
1971
+            // we are creating response from a recurring calendar item object
1972
+            // We found basedate,so opened occurrence and get properties.
1973
+            $recurr = new Recurrence($store, $this->message);
1974
+            $exception = $recurr->getExceptionAttachment($basedate);
1975
+
1976
+            if ($exception) {
1977
+                // Exception found, Now retrieve properties
1978
+                $imessage = mapi_attach_openobj($exception, 0);
1979
+                $imsgprops = mapi_getprops($imessage);
1980
+
1981
+                // If location is provided, copy it to the response
1982
+                if (isset($imsgprops[$this->proptags['location']])) {
1983
+                    $messageprops[$this->proptags['location']] = $imsgprops[$this->proptags['location']];
1984
+                }
1985
+
1986
+                // Update $messageprops with timings of occurrence
1987
+                $messageprops[$this->proptags['startdate']] = $imsgprops[$this->proptags['startdate']];
1988
+                $messageprops[$this->proptags['duedate']] = $imsgprops[$this->proptags['duedate']];
1989
+
1990
+                // Meeting related properties
1991
+                $props[$this->proptags['meetingstatus']] = $imsgprops[$this->proptags['meetingstatus']];
1992
+                $props[$this->proptags['responsestatus']] = $imsgprops[$this->proptags['responsestatus']];
1993
+                $props[PR_SUBJECT] = $imsgprops[PR_SUBJECT];
1994
+            }
1995
+            else {
1996
+                // Exceptions is deleted.
1997
+                // Update $messageprops with timings of occurrence
1998
+                $messageprops[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
1999
+                $messageprops[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
2000
+
2001
+                $props[$this->proptags['meetingstatus']] = olNonMeeting;
2002
+                $props[$this->proptags['responsestatus']] = olResponseNone;
2003
+            }
2004
+
2005
+            $props[$this->proptags['recurring']] = false;
2006
+            $props[$this->proptags['is_exception']] = true;
2007
+        }
2008
+        else {
2009
+            // we are creating a response from meeting request mail (it could be recurring or non-recurring)
2010
+            // Send all recurrence info in response, if this is a recurrence meeting.
2011
+            $isRecurring = isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']];
2012
+            $isException = isset($messageprops[$this->proptags['is_exception']]) && $messageprops[$this->proptags['is_exception']];
2013
+            if ($isRecurring || $isException) {
2014
+                if ($isRecurring) {
2015
+                    $props[$this->proptags['recurring']] = $messageprops[$this->proptags['recurring']];
2016
+                }
2017
+                if ($isException) {
2018
+                    $props[$this->proptags['is_exception']] = $messageprops[$this->proptags['is_exception']];
2019
+                }
2020
+                $calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
2021
+
2022
+                $calendaritem = mapi_msgstore_openentry($store, $calendaritems[0]);
2023
+                $recurr = new Recurrence($store, $calendaritem);
2024
+            }
2025
+        }
2026
+
2027
+        // we are sending a response for recurring meeting request (or exception), so set some required properties
2028
+        if (isset($recurr) && $recurr) {
2029
+            if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
2030
+                $props[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
2031
+            }
2032
+
2033
+            if (!empty($messageprops[$this->proptags['recurrence_data']])) {
2034
+                $props[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
2035
+            }
2036
+
2037
+            $props[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
2038
+            $props[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
2039
+
2040
+            $this->generateRecurDates($recurr, $messageprops, $props);
2041
+        }
2042
+
2043
+        // Create a response message
2044
+        $recip = [];
2045
+        $recip[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
2046
+        $recip[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2047
+        $recip[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
2048
+        $recip[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
2049
+        $recip[PR_RECIPIENT_TYPE] = MAPI_TO;
2050
+        $recip[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
2051
+
2052
+        switch ($status) {
2053
+            case olResponseAccepted:
2054
+                $classpostfix = 'Pos';
2055
+                $subjectprefix = _('Accepted');
2056
+
2057
+                break;
2058
+
2059
+            case olResponseDeclined:
2060
+                $classpostfix = 'Neg';
2061
+                $subjectprefix = _('Declined');
2062
+
2063
+                break;
2064
+
2065
+            case olResponseTentative:
2066
+                $classpostfix = 'Tent';
2067
+                $subjectprefix = _('Tentatively accepted');
2068
+
2069
+                break;
2070
+        }
2071
+
2072
+        if (!empty($proposeNewTimeProps)) {
2073
+            // if attendee has proposed new time then change subject prefix
2074
+            $subjectprefix = _('New Time Proposed');
2075
+        }
2076
+
2077
+        $props[PR_SUBJECT] = $subjectprefix . ': ' . $messageprops[PR_SUBJECT];
2078
+
2079
+        $props[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Resp.' . $classpostfix;
2080
+        if (isset($messageprops[PR_OWNER_APPT_ID])) {
2081
+            $props[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
2082
+        }
2083
+
2084
+        // Set GlobalId AND CleanGlobalId, if exception then also set basedate into GlobalId(0x3).
2085
+        $props[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
2086
+        $props[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
2087
+        $props[$this->proptags['updatecounter']] = isset($messageprops[$this->proptags['updatecounter']]) ? $messageprops[$this->proptags['updatecounter']] : 0;
2088
+
2089
+        if (!empty($proposeNewTimeProps)) {
2090
+            // merge proposal properties to message properties which will be sent to organizer
2091
+            $props = $proposeNewTimeProps + $props;
2092
+        }
2093
+
2094
+        // Set body message in Appointment
2095
+        if (isset($body)) {
2096
+            $props[PR_BODY] = $this->getMeetingTimeInfo() ? $this->getMeetingTimeInfo() : $body;
2097
+        }
2098
+
2099
+        // PR_START_DATE/PR_END_DATE is used in the UI in Outlook on the response message
2100
+        $props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
2101
+        $props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
2102
+
2103
+        // Set startdate and duedate in response mail.
2104
+        $props[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
2105
+        $props[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
2106
+
2107
+        // responselocation is used in the UI in Outlook on the response message
2108
+        if (isset($messageprops[$this->proptags['location']])) {
2109
+            $props[$this->proptags['responselocation']] = $messageprops[$this->proptags['location']];
2110
+            $props[$this->proptags['location']] = $messageprops[$this->proptags['location']];
2111
+        }
2112
+
2113
+        $message = $this->createOutgoingMessage($store);
2114
+
2115
+        mapi_setprops($message, $props);
2116
+        mapi_message_modifyrecipients($message, MODRECIP_ADD, [$recip]);
2117
+        mapi_savechanges($message);
2118
+        mapi_message_submitmessage($message);
2119
+    }
2120
+
2121
+    /**
2122
+     * Function which finds items in calendar based on globalId and cleanGlobalId.
2123
+     *
2124
+     * @param binary     $goid             GlobalID(0x3) of item
2125
+     * @param MAPIFolder $calendar         MAPI_folder of user (optional)
2126
+     * @param bool       $useCleanGlobalId if true then search should be performed on cleanGlobalId(0x23) else globalId(0x3)
2127
+     */
2128
+    public function findCalendarItems($goid, $calendar = false, $useCleanGlobalId = false) {
2129
+        if ($calendar === false) {
2130
+            // Open the Calendar
2131
+            $calendar = $this->openDefaultCalendar();
2132
+        }
2133
+
2134
+        // Find the item by restricting all items to the correct ID
2135
+        $restrict = [
2136
+            RES_AND,
2137
+            [
2138
+                [
2139
+                    RES_PROPERTY,
2140
+                    [
2141
+                        RELOP => RELOP_EQ,
2142
+                        ULPROPTAG => ($useCleanGlobalId === true ? $this->proptags['goid2'] : $this->proptags['goid']),
2143
+                        VALUE => $goid,
2144
+                    ],
2145
+                ],
2146
+            ], ];
2147
+
2148
+        $calendarcontents = mapi_folder_getcontentstable($calendar);
2149
+
2150
+        $rows = mapi_table_queryallrows($calendarcontents, [PR_ENTRYID], $restrict);
2151
+
2152
+        if (empty($rows)) {
2153
+            return;
2154
+        }
2155
+
2156
+        $calendaritems = [];
2157
+
2158
+        // In principle, there should only be one row, but we'll handle them all just in case
2159
+        foreach ($rows as $row) {
2160
+            $calendaritems[] = $row[PR_ENTRYID];
2161
+        }
2162
+
2163
+        return $calendaritems;
2164
+    }
2165
+
2166
+    // Returns TRUE if both entryid's are equal. Equality is defined by both entryid's pointing at the
2167
+    // same SMTP address when converted to SMTP
2168
+    public function compareABEntryIDs($entryid1, $entryid2) {
2169
+        // If the session was not passed, just do a 'normal' compare.
2170
+        if (!$this->session) {
2171
+            return $entryid1 == $entryid2;
2172
+        }
2173
+
2174
+        $smtp1 = $this->getSMTPAddress($entryid1);
2175
+        $smtp2 = $this->getSMTPAddress($entryid2);
2176
+
2177
+        if ($smtp1 == $smtp2) {
2178
+            return true;
2179
+        }
2180
+
2181
+        return false;
2182
+    }
2183
+
2184
+    // Gets the SMTP address of the passed addressbook entryid
2185
+    public function getSMTPAddress($entryid) {
2186
+        if (!$this->session) {
2187
+            return false;
2188
+        }
2189
+
2190
+        $ab = mapi_openaddressbook($this->session);
2191
+
2192
+        $abitem = mapi_ab_openentry($ab, $entryid);
2193
+
2194
+        if (!$abitem) {
2195
+            return '';
2196
+        }
2197
+
2198
+        $props = mapi_getprops($abitem, [PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2199
+
2200
+        if ($props[PR_ADDRTYPE] == 'SMTP') {
2201
+            return $props[PR_EMAIL_ADDRESS];
2202
+        }
2203
+
2204
+        return $props[PR_SMTP_ADDRESS];
2205
+    }
2206
+
2207
+    /**
2208
+     * Gets the properties associated with the owner of the passed store:
2209
+     * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY.
2210
+     *
2211
+     * @param $store message store
2212
+     * @param $fallbackToLoggedInUser if true then return properties of logged in user instead of mailbox owner
2213
+     * not used when passed store is public store. for public store we are always returning logged in user's info.
2214
+     *
2215
+     * @return properties of logged in user in an array in sequence of display_name, email address, address type,
2216
+     *                    entryid and search key
2217
+     */
2218
+    public function getOwnerAddress($store, $fallbackToLoggedInUser = true) {
2219
+        if (!$this->session) {
2220
+            return false;
2221
+        }
2222
+
2223
+        $storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID]);
2224
+
2225
+        $ownerEntryId = false;
2226
+        if (isset($storeProps[PR_USER_ENTRYID]) && $storeProps[PR_USER_ENTRYID]) {
2227
+            $ownerEntryId = $storeProps[PR_USER_ENTRYID];
2228
+        }
2229
+
2230
+        if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID]) && $storeProps[PR_MAILBOX_OWNER_ENTRYID] && !$fallbackToLoggedInUser) {
2231
+            $ownerEntryId = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
2232
+        }
2233
+
2234
+        if ($ownerEntryId) {
2235
+            $ab = mapi_openaddressbook($this->session);
2236
+
2237
+            $zarafaUser = mapi_ab_openentry($ab, $ownerEntryId);
2238
+            if (!$zarafaUser) {
2239
+                return false;
2240
+            }
2241
+
2242
+            $ownerProps = mapi_getprops($zarafaUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2243
+
2244
+            $addrType = $ownerProps[PR_ADDRTYPE];
2245
+            $name = $ownerProps[PR_DISPLAY_NAME];
2246
+            $emailAddr = $ownerProps[PR_EMAIL_ADDRESS];
2247
+            $searchKey = $ownerProps[PR_SEARCH_KEY];
2248
+            $entryId = $ownerEntryId;
2249
+
2250
+            return [$name, $emailAddr, $addrType, $entryId, $searchKey];
2251
+        }
2252
+
2253
+        return false;
2254
+    }
2255
+
2256
+    // Opens this session's default message store
2257
+    public function openDefaultStore() {
2258
+        $storestable = mapi_getmsgstorestable($this->session);
2259
+        $rows = mapi_table_queryallrows($storestable, [PR_ENTRYID, PR_DEFAULT_STORE]);
2260
+
2261
+        foreach ($rows as $row) {
2262
+            if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) {
2263
+                $entryid = $row[PR_ENTRYID];
2264
+
2265
+                break;
2266
+            }
2267
+        }
2268
+
2269
+        if (!$entryid) {
2270
+            return false;
2271
+        }
2272
+
2273
+        return mapi_openmsgstore($this->session, $entryid);
2274
+    }
2275
+
2276
+    /**
2277
+     *  Function which adds organizer to recipient list which is passed.
2278
+     *  This function also checks if it has organizer.
2279
+     *
2280
+     * @param array $messageProps message properties
2281
+     * @param array $recipients   recipients list of message
2282
+     * @param bool  $isException  true if we are processing recipient of exception
2283
+     */
2284
+    public function addOrganizer($messageProps, &$recipients, $isException = false) {
2285
+        $hasOrganizer = false;
2286
+        // Check if meeting already has an organizer.
2287
+        foreach ($recipients as $key => $recipient) {
2288
+            if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
2289
+                $hasOrganizer = true;
2290
+            }
2291
+            elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
2292
+                // Recipients for an occurrence
2293
+                $recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
2294
+            }
2295
+        }
2296
+
2297
+        if (!$hasOrganizer) {
2298
+            // Create organizer.
2299
+            $organizer = [];
2300
+            $organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
2301
+            $organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2302
+            $organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2303
+            $organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
2304
+            $organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2305
+            $organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
2306
+            $organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
2307
+            $organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
2308
+            $organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
2309
+
2310
+            // Add organizer to recipients list.
2311
+            array_unshift($recipients, $organizer);
2312
+        }
2313
+    }
2314
+
2315
+    /**
2316
+     * Function which removes an exception/occurrence from recurrencing meeting
2317
+     * when a meeting cancellation of an occurrence is processed.
2318
+     *
2319
+     * @param string   $basedate basedate of an occurrence
2320
+     * @param resource $message  recurring item from which occurrence has to be deleted
2321
+     * @param resource $store    MAPI_MSG_Store which contains the item
2322
+     */
2323
+    public function doRemoveExceptionFromCalendar($basedate, $message, $store) {
2324
+        $recurr = new Recurrence($store, $message);
2325
+        $recurr->createException([], $basedate, true);
2326
+        mapi_savechanges($message);
2327
+    }
2328
+
2329
+    /**
2330
+     * Function which returns basedate of an changed occurrence from globalID of meeting request.
2331
+     *
2332
+     *@param binary $goid globalID
2333
+     *
2334
+     *@return bool true if basedate is found else false it not found
2335
+     */
2336
+    public function getBasedateFromGlobalID($goid) {
2337
+        $hexguid = bin2hex($goid);
2338
+        $hexbase = substr($hexguid, 32, 8);
2339
+        $day = hexdec(substr($hexbase, 6, 2));
2340
+        $month = hexdec(substr($hexbase, 4, 2));
2341
+        $year = hexdec(substr($hexbase, 0, 4));
2342
+
2343
+        if ($day && $month && $year) {
2344
+            return gmmktime(0, 0, 0, $month, $day, $year);
2345
+        }
2346
+
2347
+        return false;
2348
+    }
2349
+
2350
+    /**
2351
+     * Function which sets basedate in globalID of changed occurrence which is to be send.
2352
+     *
2353
+     *@param binary $goid globalID
2354
+     *@param string basedate of changed occurrence
2355
+     * @param mixed $basedate
2356
+     *
2357
+     *@return binary globalID with basedate in it
2358
+     */
2359
+    public function setBasedateInGlobalID($goid, $basedate = false) {
2360
+        $hexguid = bin2hex($goid);
2361
+        $year = $basedate ? sprintf('%04s', dechex(gmdate('Y', $basedate))) : '0000';
2362
+        $month = $basedate ? sprintf('%02s', dechex(gmdate('m', $basedate))) : '00';
2363
+        $day = $basedate ? sprintf('%02s', dechex(gmdate('d', $basedate))) : '00';
2364
+
2365
+        return hex2bin(strtoupper(substr($hexguid, 0, 32) . $year . $month . $day . substr($hexguid, 40)));
2366
+    }
2367
+
2368
+    /**
2369
+     * Function which replaces attachments with copy_from in copy_to.
2370
+     *
2371
+     * @param MAPIMessage $copy_from      MAPI_message from which attachments are to be copied
2372
+     * @param MAPIMessage $copy_to        MAPI_message to which attachment are to be copied
2373
+     * @param bool        $copyExceptions if true then all exceptions should also be sent as attachments
2374
+     * @param mixed       $copyFrom
2375
+     * @param mixed       $copyTo
2376
+     */
2377
+    public function replaceAttachments($copyFrom, $copyTo, $copyExceptions = true) {
2378
+        /* remove all old attachments */
2379
+        $attachmentTable = mapi_message_getattachmenttable($copyTo);
2380
+        if ($attachmentTable) {
2381
+            $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2382
+
2383
+            foreach ($attachments as $attachProps) {
2384
+                /* remove exceptions too? */
2385
+                if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2386
+                    continue;
2387
+                }
2388
+                mapi_message_deleteattach($copyTo, $attachProps[PR_ATTACH_NUM]);
2389
+            }
2390
+        }
2391
+        $attachmentTable = false;
2392
+
2393
+        /* copy new attachments */
2394
+        $attachmentTable = mapi_message_getattachmenttable($copyFrom);
2395
+        if ($attachmentTable) {
2396
+            $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2397
+
2398
+            foreach ($attachments as $attachProps) {
2399
+                if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2400
+                    continue;
2401
+                }
2402
+
2403
+                $attachOld = mapi_message_openattach($copyFrom, (int) $attachProps[PR_ATTACH_NUM]);
2404
+                $attachNewResourceMsg = mapi_message_createattach($copyTo);
2405
+                mapi_copyto($attachOld, [], [], $attachNewResourceMsg, 0);
2406
+                mapi_savechanges($attachNewResourceMsg);
2407
+            }
2408
+        }
2409
+    }
2410
+
2411
+    /**
2412
+     * Function which replaces recipients in copy_to with recipients from copyFrom.
2413
+     *
2414
+     * @param MAPIMessage $copyFrom   MAPI_message from which recipients are to be copied
2415
+     * @param MAPIMessage $copyTo     MAPI_message to which recipients are to be copied
2416
+     * @param bool        $isDelegate indicates delegate is processing
2417
+     *                                so don't copy delegate information to recipient table
2418
+     */
2419
+    public function replaceRecipients($copyFrom, $copyTo, $isDelegate = false) {
2420
+        $recipientTable = mapi_message_getrecipienttable($copyFrom);
2421
+
2422
+        // If delegate, then do not add the delegate in recipients
2423
+        if ($isDelegate) {
2424
+            $delegate = mapi_getprops($copyFrom, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2425
+            $res = [RES_PROPERTY, [
2426
+                RELOP => RELOP_NE,
2427
+                ULPROPTAG => PR_EMAIL_ADDRESS,
2428
+                VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2429
+            ],
2430
+            ];
2431
+            $recipients = mapi_table_queryallrows($recipientTable, $this->recipprops, $res);
2432
+        }
2433
+        else {
2434
+            $recipients = mapi_table_queryallrows($recipientTable, $this->recipprops);
2435
+        }
2436
+
2437
+        $copyToRecipientTable = mapi_message_getrecipienttable($copyTo);
2438
+        $copyToRecipientRows = mapi_table_queryallrows($copyToRecipientTable, [PR_ROWID]);
2439
+
2440
+        mapi_message_modifyrecipients($copyTo, MODRECIP_REMOVE, $copyToRecipientRows);
2441
+        mapi_message_modifyrecipients($copyTo, MODRECIP_ADD, $recipients);
2442
+    }
2443
+
2444
+    /**
2445
+     * Function creates meeting item in resource's calendar.
2446
+     *
2447
+     * @param resource $message  MAPI_message which is to create in resource's calendar
2448
+     * @param bool     $cancel   cancel meeting
2449
+     * @param string   $prefix   prefix for subject of meeting
2450
+     * @param mixed    $basedate
2451
+     */
2452
+    public function bookResources($message, $cancel, $prefix, $basedate = false) {
2453
+        if (!$this->enableDirectBooking) {
2454
+            return [];
2455
+        }
2456
+
2457
+        // Get the properties of the message
2458
+        $messageprops = mapi_getprops($message);
2459
+
2460
+        if ($basedate) {
2461
+            $recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID]);
2462
+
2463
+            $messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedate);
2464
+            $messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
2465
+
2466
+            // Delete properties which are not needed.
2467
+            $deleteProps = [$this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD];
2468
+            foreach ($deleteProps as $propID) {
2469
+                if (isset($messageprops[$propID])) {
2470
+                    unset($messageprops[$propID]);
2471
+                }
2472
+            }
2473
+
2474
+            if (isset($messageprops[$this->proptags['recurring']])) {
2475
+                $messageprops[$this->proptags['recurring']] = false;
2476
+            }
2477
+
2478
+            // Set Outlook properties
2479
+            $messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']];
2480
+            $messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']];
2481
+            $messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']];
2482
+            $messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']];
2483
+            $messageprops[$this->proptags['attendee_critical_change']] = time();
2484
+            $messageprops[$this->proptags['owner_critical_change']] = time();
2485
+        }
2486
+
2487
+        // Get resource recipients
2488
+        $getResourcesRestriction = [
2489
+            RES_AND,
2490
+            [
2491
+                [
2492
+                    RES_PROPERTY,
2493
+                    [
2494
+                        RELOP => RELOP_EQ,    // Equals recipient type 3: Resource
2495
+                        ULPROPTAG => PR_RECIPIENT_TYPE,
2496
+                        VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2497
+                    ],
2498
+                ], ],
2499
+        ];
2500
+        $recipienttable = mapi_message_getrecipienttable($message);
2501
+        $resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2502
+
2503
+        $this->errorSetResource = false;
2504
+        $resourceRecipData = [];
2505
+
2506
+        // Put appointment into store resource users
2507
+        $i = 0;
2508
+        $len = count($resourceRecipients);
2509
+        while (!$this->errorSetResource && $i < $len) {
2510
+            $userStore = $this->openCustomUserStore($resourceRecipients[$i][PR_ENTRYID]);
2511
+
2512
+            // Open root folder
2513
+            $userRoot = mapi_msgstore_openentry($userStore, null);
2514
+
2515
+            // Get calendar entryID
2516
+            $userRootProps = mapi_getprops($userRoot, [PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS]);
2517
+
2518
+            // Open Calendar folder
2519
+            $accessToFolder = false;
2520
+
2521
+            try {
2522
+                // @FIXME this checks delegate has access to resource's calendar folder
2523
+                // but it should use boss' credentials
2524
+
2525
+                $accessToFolder = $this->checkCalendarWriteAccess($this->store);
2526
+                if ($accessToFolder) {
2527
+                    $calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
2528
+                }
2529
+            }
2530
+            catch (MAPIException $e) {
2531
+                $e->setHandled();
2532
+                $this->errorSetResource = 1; // No access
2533
+            }
2534
+
2535
+            if ($accessToFolder) {
2536
+                /**
2537
+                 * Get the LocalFreebusy message that contains the properties that
2538
+                 * are set to accept or decline resource meeting requests.
2539
+                 */
2540
+                // Use PR_FREEBUSY_ENTRYIDS[1] to open folder the LocalFreeBusy msg
2541
+                $localFreebusyMsg = mapi_msgstore_openentry($userStore, $userRootProps[PR_FREEBUSY_ENTRYIDS][1]);
2542
+                if ($localFreebusyMsg) {
2543
+                    $props = mapi_getprops($localFreebusyMsg, [PR_PROCESS_MEETING_REQUESTS, PR_DECLINE_RECURRING_MEETING_REQUESTS, PR_DECLINE_CONFLICTING_MEETING_REQUESTS]);
2544
+
2545
+                    $acceptMeetingRequests = isset($props[PR_PROCESS_MEETING_REQUESTS]) ? $props[PR_PROCESS_MEETING_REQUESTS] : false;
2546
+                    $declineRecurringMeetingRequests = isset($props[PR_DECLINE_RECURRING_MEETING_REQUESTS]) ? $props[PR_DECLINE_RECURRING_MEETING_REQUESTS] : false;
2547
+                    $declineConflictingMeetingRequests = isset($props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS]) ? $props[PR_DECLINE_CONFLICTING_MEETING_REQUESTS] : false;
2548
+
2549
+                    if (!$acceptMeetingRequests) {
2550
+                        /*
2551 2551
 						 * When a resource has not been set to automatically accept meeting requests,
2552 2552
 						 * the meeting request has to be sent to him rather than being put directly into
2553 2553
 						 * his calendar. No error should be returned.
2554 2554
 						 */
2555
-						// $errorSetResource = 2;
2556
-						$this->nonAcceptingResources[] = $resourceRecipients[$i];
2557
-					}
2558
-					else {
2559
-						if ($declineRecurringMeetingRequests && !$cancel) {
2560
-							// Check if appointment is recurring
2561
-							if ($messageprops[$this->proptags['recurring']]) {
2562
-								$this->errorSetResource = 3;
2563
-							}
2564
-						}
2565
-						if ($declineConflictingMeetingRequests && !$cancel) {
2566
-							// Check for conflicting items
2567
-							if ($calFolder && $this->isMeetingConflicting($message, $userStore, $calFolder)) {
2568
-								$this->errorSetResource = 4; // Conflict
2569
-							}
2570
-						}
2571
-					}
2572
-				}
2573
-			}
2574
-
2575
-			if (!$this->errorSetResource && $accessToFolder) {
2576
-				/**
2577
-				 * First search on GlobalID(0x3)
2578
-				 * 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.
2579
-				 * If (normal meeting) then GlobalID(0x3) and CleanGlobalID(0x23) are same, so doesn't matter if search is based on GlobalID.
2580
-				 */
2581
-				$rows = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
2582
-
2583
-				/*
2555
+                        // $errorSetResource = 2;
2556
+                        $this->nonAcceptingResources[] = $resourceRecipients[$i];
2557
+                    }
2558
+                    else {
2559
+                        if ($declineRecurringMeetingRequests && !$cancel) {
2560
+                            // Check if appointment is recurring
2561
+                            if ($messageprops[$this->proptags['recurring']]) {
2562
+                                $this->errorSetResource = 3;
2563
+                            }
2564
+                        }
2565
+                        if ($declineConflictingMeetingRequests && !$cancel) {
2566
+                            // Check for conflicting items
2567
+                            if ($calFolder && $this->isMeetingConflicting($message, $userStore, $calFolder)) {
2568
+                                $this->errorSetResource = 4; // Conflict
2569
+                            }
2570
+                        }
2571
+                    }
2572
+                }
2573
+            }
2574
+
2575
+            if (!$this->errorSetResource && $accessToFolder) {
2576
+                /**
2577
+                 * First search on GlobalID(0x3)
2578
+                 * 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.
2579
+                 * If (normal meeting) then GlobalID(0x3) and CleanGlobalID(0x23) are same, so doesn't matter if search is based on GlobalID.
2580
+                 */
2581
+                $rows = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
2582
+
2583
+                /*
2584 2584
 				 * If no entry is found then
2585 2585
 				 * 1) Resource doesn't have meeting in Calendar. Seriously!!
2586 2586
 				 * OR
2587 2587
 				 * 2) We were looking for occurrence item but Resource has whole series
2588 2588
 				 */
2589
-				if (empty($rows)) {
2590
-					/**
2591
-					 * Now search on CleanGlobalID(0x23) WHY???
2592
-					 * Because we are looking recurring item.
2593
-					 *
2594
-					 * Possible results of this search
2595
-					 * 1) If Resource was booked for more than one occurrences then this search will return all those occurrence because search is perform on CleanGlobalID
2596
-					 * 2) If Resource was booked for whole series then it should return series.
2597
-					 */
2598
-					$rows = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
2599
-
2600
-					$newResourceMsg = false;
2601
-					if (!empty($rows)) {
2602
-						// Since we are looking for recurring item, open every result and check for 'recurring' property.
2603
-						foreach ($rows as $row) {
2604
-							$ResourceMsg = mapi_msgstore_openentry($userStore, $row);
2605
-							$ResourceMsgProps = mapi_getprops($ResourceMsg, [$this->proptags['recurring']]);
2606
-
2607
-							if (isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2608
-								$newResourceMsg = $ResourceMsg;
2609
-
2610
-								break;
2611
-							}
2612
-						}
2613
-					}
2614
-
2615
-					// Still no results found. I giveup, create new message.
2616
-					if (!$newResourceMsg) {
2617
-						$newResourceMsg = mapi_folder_createmessage($calFolder);
2618
-					}
2619
-				}
2620
-				else {
2621
-					$newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]);
2622
-				}
2623
-
2624
-				// Prefix the subject if needed
2625
-				if ($prefix && isset($messageprops[PR_SUBJECT])) {
2626
-					$messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT];
2627
-				}
2628
-
2629
-				// Set status to cancelled if needed
2630
-				$messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy)
2631
-				if ($cancel) {
2632
-					$messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled
2633
-					$messageprops[$this->proptags['busystatus']] = fbFree; // Free
2634
-				}
2635
-				else {
2636
-					$messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
2637
-				}
2638
-				$messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource automatically accepts the appointment
2639
-
2640
-				$messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment';
2641
-
2642
-				// Remove the PR_ICON_INDEX as it is not needed in the sent message.
2643
-				$messageprops[PR_ICON_INDEX] = null;
2644
-				$messageprops[PR_RESPONSE_REQUESTED] = true;
2645
-
2646
-				// get the store of organizer, in case of delegates it will be delegate store
2647
-				$defaultStore = $this->openDefaultStore();
2648
-
2649
-				$storeProps = mapi_getprops($this->store, [PR_ENTRYID]);
2650
-				$defaultStoreProps = mapi_getprops($defaultStore, [PR_ENTRYID]);
2651
-
2652
-				// @FIXME use entryid comparison functions here
2653
-				if ($storeProps[PR_ENTRYID] !== $defaultStoreProps[PR_ENTRYID]) {
2654
-					// get delegate information
2655
-					$addrInfo = $this->getOwnerAddress($defaultStore, false);
2656
-
2657
-					if ($addrInfo) {
2658
-						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2659
-
2660
-						$messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2661
-						$messageprops[PR_SENDER_NAME] = $ownername;
2662
-						$messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2663
-						$messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2664
-						$messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2665
-					}
2666
-
2667
-					// get delegator information
2668
-					$addrInfo = $this->getOwnerAddress($this->store, false);
2669
-
2670
-					if ($addrInfo) {
2671
-						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2672
-
2673
-						$messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2674
-						$messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2675
-						$messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2676
-						$messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2677
-						$messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2678
-					}
2679
-				}
2680
-				else {
2681
-					// get organizer information
2682
-					$addrinfo = $this->getOwnerAddress($this->store);
2683
-
2684
-					if ($addrinfo) {
2685
-						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrinfo;
2686
-
2687
-						$messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2688
-						$messageprops[PR_SENDER_NAME] = $ownername;
2689
-						$messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2690
-						$messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2691
-						$messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2692
-
2693
-						$messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2694
-						$messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2695
-						$messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2696
-						$messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2697
-						$messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2698
-					}
2699
-				}
2700
-
2701
-				$messageprops[$this->proptags['replytime']] = time();
2702
-
2703
-				if ($basedate && isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2704
-					$recurr = new Recurrence($userStore, $newResourceMsg);
2705
-
2706
-					// Copy recipients list
2707
-					$reciptable = mapi_message_getrecipienttable($message);
2708
-					$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2709
-
2710
-					// add owner to recipient table
2711
-					$this->addOrganizer($messageprops, $recips, true);
2712
-
2713
-					// Update occurrence
2714
-					if ($recurr->isException($basedate)) {
2715
-						$recurr->modifyException($messageprops, $basedate, $recips);
2716
-					}
2717
-					else {
2718
-						$recurr->createException($messageprops, $basedate, false, $recips);
2719
-					}
2720
-				}
2721
-				else {
2722
-					mapi_setprops($newResourceMsg, $messageprops);
2723
-
2724
-					// Copy attachments
2725
-					$this->replaceAttachments($message, $newResourceMsg);
2726
-
2727
-					// Copy all recipients too
2728
-					$this->replaceRecipients($message, $newResourceMsg);
2729
-
2730
-					// Now add organizer also to recipient table
2731
-					$recips = [];
2732
-					$this->addOrganizer($messageprops, $recips);
2733
-
2734
-					mapi_message_modifyrecipients($newResourceMsg, MODRECIP_ADD, $recips);
2735
-				}
2736
-
2737
-				mapi_savechanges($newResourceMsg);
2738
-
2739
-				$resourceRecipData[] = [
2740
-					'store' => $userStore,
2741
-					'folder' => $calFolder,
2742
-					'msg' => $newResourceMsg,
2743
-				];
2744
-				$this->includesResources = true;
2745
-			}
2746
-			else {
2747
-				/*
2589
+                if (empty($rows)) {
2590
+                    /**
2591
+                     * Now search on CleanGlobalID(0x23) WHY???
2592
+                     * Because we are looking recurring item.
2593
+                     *
2594
+                     * Possible results of this search
2595
+                     * 1) If Resource was booked for more than one occurrences then this search will return all those occurrence because search is perform on CleanGlobalID
2596
+                     * 2) If Resource was booked for whole series then it should return series.
2597
+                     */
2598
+                    $rows = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
2599
+
2600
+                    $newResourceMsg = false;
2601
+                    if (!empty($rows)) {
2602
+                        // Since we are looking for recurring item, open every result and check for 'recurring' property.
2603
+                        foreach ($rows as $row) {
2604
+                            $ResourceMsg = mapi_msgstore_openentry($userStore, $row);
2605
+                            $ResourceMsgProps = mapi_getprops($ResourceMsg, [$this->proptags['recurring']]);
2606
+
2607
+                            if (isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2608
+                                $newResourceMsg = $ResourceMsg;
2609
+
2610
+                                break;
2611
+                            }
2612
+                        }
2613
+                    }
2614
+
2615
+                    // Still no results found. I giveup, create new message.
2616
+                    if (!$newResourceMsg) {
2617
+                        $newResourceMsg = mapi_folder_createmessage($calFolder);
2618
+                    }
2619
+                }
2620
+                else {
2621
+                    $newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]);
2622
+                }
2623
+
2624
+                // Prefix the subject if needed
2625
+                if ($prefix && isset($messageprops[PR_SUBJECT])) {
2626
+                    $messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT];
2627
+                }
2628
+
2629
+                // Set status to cancelled if needed
2630
+                $messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy)
2631
+                if ($cancel) {
2632
+                    $messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled
2633
+                    $messageprops[$this->proptags['busystatus']] = fbFree; // Free
2634
+                }
2635
+                else {
2636
+                    $messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
2637
+                }
2638
+                $messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource automatically accepts the appointment
2639
+
2640
+                $messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment';
2641
+
2642
+                // Remove the PR_ICON_INDEX as it is not needed in the sent message.
2643
+                $messageprops[PR_ICON_INDEX] = null;
2644
+                $messageprops[PR_RESPONSE_REQUESTED] = true;
2645
+
2646
+                // get the store of organizer, in case of delegates it will be delegate store
2647
+                $defaultStore = $this->openDefaultStore();
2648
+
2649
+                $storeProps = mapi_getprops($this->store, [PR_ENTRYID]);
2650
+                $defaultStoreProps = mapi_getprops($defaultStore, [PR_ENTRYID]);
2651
+
2652
+                // @FIXME use entryid comparison functions here
2653
+                if ($storeProps[PR_ENTRYID] !== $defaultStoreProps[PR_ENTRYID]) {
2654
+                    // get delegate information
2655
+                    $addrInfo = $this->getOwnerAddress($defaultStore, false);
2656
+
2657
+                    if ($addrInfo) {
2658
+                        list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2659
+
2660
+                        $messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2661
+                        $messageprops[PR_SENDER_NAME] = $ownername;
2662
+                        $messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2663
+                        $messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2664
+                        $messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2665
+                    }
2666
+
2667
+                    // get delegator information
2668
+                    $addrInfo = $this->getOwnerAddress($this->store, false);
2669
+
2670
+                    if ($addrInfo) {
2671
+                        list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2672
+
2673
+                        $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2674
+                        $messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2675
+                        $messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2676
+                        $messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2677
+                        $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2678
+                    }
2679
+                }
2680
+                else {
2681
+                    // get organizer information
2682
+                    $addrinfo = $this->getOwnerAddress($this->store);
2683
+
2684
+                    if ($addrinfo) {
2685
+                        list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrinfo;
2686
+
2687
+                        $messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2688
+                        $messageprops[PR_SENDER_NAME] = $ownername;
2689
+                        $messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2690
+                        $messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2691
+                        $messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2692
+
2693
+                        $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2694
+                        $messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2695
+                        $messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2696
+                        $messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2697
+                        $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2698
+                    }
2699
+                }
2700
+
2701
+                $messageprops[$this->proptags['replytime']] = time();
2702
+
2703
+                if ($basedate && isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2704
+                    $recurr = new Recurrence($userStore, $newResourceMsg);
2705
+
2706
+                    // Copy recipients list
2707
+                    $reciptable = mapi_message_getrecipienttable($message);
2708
+                    $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2709
+
2710
+                    // add owner to recipient table
2711
+                    $this->addOrganizer($messageprops, $recips, true);
2712
+
2713
+                    // Update occurrence
2714
+                    if ($recurr->isException($basedate)) {
2715
+                        $recurr->modifyException($messageprops, $basedate, $recips);
2716
+                    }
2717
+                    else {
2718
+                        $recurr->createException($messageprops, $basedate, false, $recips);
2719
+                    }
2720
+                }
2721
+                else {
2722
+                    mapi_setprops($newResourceMsg, $messageprops);
2723
+
2724
+                    // Copy attachments
2725
+                    $this->replaceAttachments($message, $newResourceMsg);
2726
+
2727
+                    // Copy all recipients too
2728
+                    $this->replaceRecipients($message, $newResourceMsg);
2729
+
2730
+                    // Now add organizer also to recipient table
2731
+                    $recips = [];
2732
+                    $this->addOrganizer($messageprops, $recips);
2733
+
2734
+                    mapi_message_modifyrecipients($newResourceMsg, MODRECIP_ADD, $recips);
2735
+                }
2736
+
2737
+                mapi_savechanges($newResourceMsg);
2738
+
2739
+                $resourceRecipData[] = [
2740
+                    'store' => $userStore,
2741
+                    'folder' => $calFolder,
2742
+                    'msg' => $newResourceMsg,
2743
+                ];
2744
+                $this->includesResources = true;
2745
+            }
2746
+            else {
2747
+                /*
2748 2748
 				 * If no other errors occurred and you have no access to the
2749 2749
 				 * folder of the resource, throw an error=1.
2750 2750
 				 */
2751
-				if (!$this->errorSetResource) {
2752
-					$this->errorSetResource = 1;
2753
-				}
2754
-
2755
-				for ($j = 0, $len = count($resourceRecipData); $j < $len; ++$j) {
2756
-					// Get the EntryID
2757
-					$props = mapi_message_getprops($resourceRecipData[$j]['msg']);
2758
-
2759
-					mapi_folder_deletemessages($resourceRecipData[$j]['folder'], [$props[PR_ENTRYID]], DELETE_HARD_DELETE);
2760
-				}
2761
-				$this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME];
2762
-			}
2763
-			++$i;
2764
-		}
2765
-
2766
-		/*
2751
+                if (!$this->errorSetResource) {
2752
+                    $this->errorSetResource = 1;
2753
+                }
2754
+
2755
+                for ($j = 0, $len = count($resourceRecipData); $j < $len; ++$j) {
2756
+                    // Get the EntryID
2757
+                    $props = mapi_message_getprops($resourceRecipData[$j]['msg']);
2758
+
2759
+                    mapi_folder_deletemessages($resourceRecipData[$j]['folder'], [$props[PR_ENTRYID]], DELETE_HARD_DELETE);
2760
+                }
2761
+                $this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME];
2762
+            }
2763
+            ++$i;
2764
+        }
2765
+
2766
+        /*
2767 2767
 		 * Set the BCC-recipients (resources) tackstatus to accepted.
2768 2768
 		 */
2769
-		// Get resource recipients
2770
-		$getResourcesRestriction = [
2771
-			RES_AND,
2772
-			[
2773
-				[
2774
-					RES_PROPERTY,
2775
-					[
2776
-						RELOP => RELOP_EQ,    // Equals recipient type 3: Resource
2777
-						ULPROPTAG => PR_RECIPIENT_TYPE,
2778
-						VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2779
-					],
2780
-				], ],
2781
-		];
2782
-		$recipienttable = mapi_message_getrecipienttable($message);
2783
-		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2784
-		if (!empty($resourceRecipients)) {
2785
-			// Set Tracking status of resource recipients to olResponseAccepted (3)
2786
-			for ($i = 0, $len = count($resourceRecipients); $i < $len; ++$i) {
2787
-				$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted;
2788
-				$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time();
2789
-			}
2790
-			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $resourceRecipients);
2791
-		}
2792
-
2793
-		// Publish updated free/busy information
2794
-		if (!$this->errorSetResource) {
2795
-			for ($i = 0, $len = count($resourceRecipData); $i < $len; ++$i) {
2796
-				$storeProps = mapi_getprops($resourceRecipData[$i]['store'], [PR_MAILBOX_OWNER_ENTRYID]);
2797
-				if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) {
2798
-					$start = time() - 7 * 24 * 60 * 60;
2799
-					$range = strtotime("+6 month");
2800
-					$range = $range - (7 * 24 * 60 * 60);
2801
-
2802
-					$pub = new FreeBusyPublish($this->session, $resourceRecipData[$i]['store'], $resourceRecipData[$i]['folder'], $storeProps[PR_MAILBOX_OWNER_ENTRYID]);
2803
-					$pub->publishFB($start, $range); // publish from one week ago, 6 months ahead
2804
-				}
2805
-			}
2806
-		}
2807
-
2808
-		return $resourceRecipData;
2809
-	}
2810
-
2811
-	/**
2812
-	 * Function which save an exception into recurring item.
2813
-	 *
2814
-	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2815
-	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2816
-	 * @param string   $basedate       basedate of occurrence
2817
-	 * @param bool     $move           if true then occurrence item is deleted
2818
-	 * @param bool     $tentative      true if user has tentatively accepted it or false if user has accepted it
2819
-	 * @param bool     $userAction     true if user has manually responded to meeting request
2820
-	 * @param resource $store          user store
2821
-	 * @param bool     $isDelegate     true if delegate is processing this meeting request
2822
-	 */
2823
-	public function acceptException(&$recurringItem, &$occurrenceItem, $basedate, $move = false, $tentative, $userAction = false, $store, $isDelegate = false) {
2824
-		$recurr = new Recurrence($store, $recurringItem);
2825
-
2826
-		// Copy properties from meeting request
2827
-		$exception_props = mapi_getprops($occurrenceItem);
2828
-
2829
-		// Copy recipients list
2830
-		$reciptable = mapi_message_getrecipienttable($occurrenceItem);
2831
-		// If delegate, then do not add the delegate in recipients
2832
-		if ($isDelegate) {
2833
-			$delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2834
-			$res = [RES_PROPERTY, [
2835
-				RELOP => RELOP_NE,
2836
-				ULPROPTAG => PR_EMAIL_ADDRESS,
2837
-				VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2838
-			],
2839
-			];
2840
-			$recips = mapi_table_queryallrows($reciptable, $this->recipprops, $res);
2841
-		}
2842
-		else {
2843
-			$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2844
-		}
2845
-
2846
-		// add owner to recipient table
2847
-		$this->addOrganizer($exception_props, $recips, true);
2848
-
2849
-		// add delegator to meetings
2850
-		if ($isDelegate) {
2851
-			$this->addDelegator($exception_props, $recips);
2852
-		}
2853
-
2854
-		$exception_props[$this->proptags['meetingstatus']] = olMeetingReceived;
2855
-		$exception_props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
2856
-
2857
-		if (isset($exception_props[$this->proptags['intendedbusystatus']])) {
2858
-			if ($tentative && $exception_props[$this->proptags['intendedbusystatus']] !== fbFree) {
2859
-				$exception_props[$this->proptags['busystatus']] = fbTentative;
2860
-			}
2861
-			else {
2862
-				$exception_props[$this->proptags['busystatus']] = $exception_props[$this->proptags['intendedbusystatus']];
2863
-			}
2864
-			// we already have intendedbusystatus value in $exception_props so no need to copy it
2865
-		}
2866
-		else {
2867
-			$exception_props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
2868
-		}
2869
-
2870
-		if ($userAction) {
2871
-			$addrInfo = $this->getOwnerAddress($this->store);
2872
-
2873
-			// if user has responded then set replytime and name
2874
-			$exception_props[$this->proptags['replytime']] = time();
2875
-			if (!empty($addrInfo)) {
2876
-				$exception_props[$this->proptags['apptreplyname']] = $addrInfo[0];
2877
-			}
2878
-		}
2879
-
2880
-		if ($recurr->isException($basedate)) {
2881
-			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2882
-		}
2883
-		else {
2884
-			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2885
-		}
2886
-
2887
-		// Move the occurrenceItem to the waste basket
2888
-		if ($move) {
2889
-			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2890
-			$sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
2891
-			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
2892
-		}
2893
-
2894
-		mapi_savechanges($recurringItem);
2895
-	}
2896
-
2897
-	/**
2898
-	 * Function which merges an exception mapi message to recurring message.
2899
-	 * This will be used when we receive recurring meeting request and we already have an exception message
2900
-	 * of same meeting in calendar and we need to remove that exception message and add it to attachment table
2901
-	 * of recurring meeting.
2902
-	 *
2903
-	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2904
-	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2905
-	 * @param string   $basedate       basedate of occurrence
2906
-	 * @param resource $store          user store
2907
-	 */
2908
-	public function mergeException(&$recurringItem, &$occurrenceItem, $basedate, $store) {
2909
-		$recurr = new Recurrence($store, $recurringItem);
2910
-
2911
-		// Copy properties from meeting request
2912
-		$exception_props = mapi_getprops($occurrenceItem);
2913
-
2914
-		// Get recipient list from message and add it to exception attachment
2915
-		$reciptable = mapi_message_getrecipienttable($occurrenceItem);
2916
-		$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2917
-
2918
-		if ($recurr->isException($basedate)) {
2919
-			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2920
-		}
2921
-		else {
2922
-			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2923
-		}
2924
-
2925
-		// Move the occurrenceItem to the waste basket
2926
-		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2927
-		$sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
2928
-		mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
2929
-
2930
-		mapi_savechanges($recurringItem);
2931
-	}
2932
-
2933
-	/**
2934
-	 * Function which submits meeting request based on arguments passed to it.
2935
-	 *
2936
-	 * @param resource $message        MAPI_message whose meeting request is to be send
2937
-	 * @param bool     $cancel         if true send request, else send cancellation
2938
-	 * @param string   $prefix         subject prefix
2939
-	 * @param int      $basedate       basedate for an occurrence
2940
-	 * @param object   $recurObject    recurrence object of mr
2941
-	 * @param bool     $copyExceptions When sending update mail for recurring item then we dont send exceptions in attachments
2942
-	 * @param mixed    $modifiedRecips
2943
-	 * @param mixed    $deletedRecips
2944
-	 */
2945
-	public function submitMeetingRequest($message, $cancel, $prefix, $basedate = false, $recurObject = false, $copyExceptions = true, $modifiedRecips = false, $deletedRecips = false) {
2946
-		$newmessageprops = $messageprops = mapi_getprops($this->message);
2947
-		$new = $this->createOutgoingMessage();
2948
-
2949
-		// Copy the entire message into the new meeting request message
2950
-		if ($basedate) {
2951
-			// messageprops contains properties of whole recurring series
2952
-			// and newmessageprops contains properties of exception item
2953
-			$newmessageprops = mapi_getprops($message);
2954
-
2955
-			// Ensure that the correct basedate is set in the new message
2956
-			$newmessageprops[$this->proptags['basedate']] = $basedate;
2957
-
2958
-			// Set isRecurring to false, because this is an exception
2959
-			$newmessageprops[$this->proptags['recurring']] = false;
2960
-
2961
-			// set LID_IS_EXCEPTION to true
2962
-			$newmessageprops[$this->proptags['is_exception']] = true;
2963
-
2964
-			// Set to high importance
2965
-			if ($cancel) {
2966
-				$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;
2967
-			}
2968
-
2969
-			// Set startdate and enddate of exception
2970
-			if ($cancel && $recurObject) {
2971
-				$newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
2972
-				$newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
2973
-			}
2974
-
2975
-			// Set basedate in guid (0x3)
2976
-			$newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
2977
-			$newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
2978
-			$newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
2979
-
2980
-			// Get deleted recipiets from exception msg
2981
-			$restriction = [
2982
-				RES_AND,
2983
-				[
2984
-					[
2985
-						RES_BITMASK,
2986
-						[
2987
-							ULTYPE => BMR_NEZ,
2988
-							ULPROPTAG => PR_RECIPIENT_FLAGS,
2989
-							ULMASK => recipExceptionalDeleted,
2990
-						],
2991
-					],
2992
-					[
2993
-						RES_BITMASK,
2994
-						[
2995
-							ULTYPE => BMR_EQZ,
2996
-							ULPROPTAG => PR_RECIPIENT_FLAGS,
2997
-							ULMASK => recipOrganizer,
2998
-						],
2999
-					],
3000
-				],
3001
-			];
3002
-
3003
-			// In direct-booking mode, we don't need to send cancellations to resources
3004
-			if ($this->enableDirectBooking) {
3005
-				$restriction[1][] = [
3006
-					RES_PROPERTY,
3007
-					[
3008
-						RELOP => RELOP_NE,    // Does not equal recipient type: MAPI_BCC (Resource)
3009
-						ULPROPTAG => PR_RECIPIENT_TYPE,
3010
-						VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3011
-					],
3012
-				];
3013
-			}
3014
-
3015
-			$recipienttable = mapi_message_getrecipienttable($message);
3016
-			$recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $restriction);
3017
-
3018
-			if (!$deletedRecips) {
3019
-				$deletedRecips = array_merge([], $recipients);
3020
-			}
3021
-			else {
3022
-				$deletedRecips = array_merge($deletedRecips, $recipients);
3023
-			}
3024
-		}
3025
-
3026
-		// Remove the PR_ICON_INDEX as it is not needed in the sent message.
3027
-		$newmessageprops[PR_ICON_INDEX] = null;
3028
-		$newmessageprops[PR_RESPONSE_REQUESTED] = true;
3029
-
3030
-		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3031
-		$newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']];
3032
-		$newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']];
3033
-
3034
-		// Set updatecounter/AppointmentSequenceNumber
3035
-		// get the value of latest updatecounter for the whole series and use it
3036
-		$newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']];
3037
-
3038
-		$meetingTimeInfo = $this->getMeetingTimeInfo();
3039
-
3040
-		if ($meetingTimeInfo) {
3041
-			$newmessageprops[PR_BODY] = $meetingTimeInfo;
3042
-		}
3043
-
3044
-		// Send all recurrence info in mail, if this is a recurrence meeting.
3045
-		if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']]) {
3046
-			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
3047
-				$newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
3048
-			}
3049
-			$newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
3050
-			$newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
3051
-			$newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
3052
-
3053
-			if ($recurObject) {
3054
-				$this->generateRecurDates($recurObject, $messageprops, $newmessageprops);
3055
-			}
3056
-		}
3057
-
3058
-		if (isset($newmessageprops[$this->proptags['counter_proposal']])) {
3059
-			unset($newmessageprops[$this->proptags['counter_proposal']]);
3060
-		}
3061
-
3062
-		// Prefix the subject if needed
3063
-		if ($prefix && isset($newmessageprops[PR_SUBJECT])) {
3064
-			$newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT];
3065
-		}
3066
-
3067
-		if (isset($newmessageprops[$this->proptags['categories']]) &&
3068
-			!empty($newmessageprops[$this->proptags['categories']])) {
3069
-			unset($newmessageprops[$this->proptags['categories']]);
3070
-		}
3071
-		mapi_setprops($new, $newmessageprops);
3072
-
3073
-		// Copy attachments
3074
-		$this->replaceAttachments($message, $new, $copyExceptions);
3075
-
3076
-		// Retrieve only those recipient who should receive this meeting request.
3077
-		$stripResourcesRestriction = [
3078
-			RES_AND,
3079
-			[
3080
-				[
3081
-					RES_BITMASK,
3082
-					[
3083
-						ULTYPE => BMR_EQZ,
3084
-						ULPROPTAG => PR_RECIPIENT_FLAGS,
3085
-						ULMASK => recipExceptionalDeleted,
3086
-					],
3087
-				],
3088
-				[
3089
-					RES_BITMASK,
3090
-					[
3091
-						ULTYPE => BMR_EQZ,
3092
-						ULPROPTAG => PR_RECIPIENT_FLAGS,
3093
-						ULMASK => recipOrganizer,
3094
-					],
3095
-				],
3096
-			],
3097
-		];
3098
-
3099
-		// In direct-booking mode, resources do not receive a meeting request
3100
-		if ($this->enableDirectBooking) {
3101
-			$stripResourcesRestriction[1][] =
3102
-									[
3103
-										RES_PROPERTY,
3104
-										[
3105
-											RELOP => RELOP_NE,    // Does not equal recipient type: MAPI_BCC (Resource)
3106
-											ULPROPTAG => PR_RECIPIENT_TYPE,
3107
-											VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3108
-										],
3109
-									];
3110
-		}
3111
-
3112
-		// If no recipients were explicitly provided, we will send the update to all
3113
-		// recipients from the meeting.
3114
-		if ($modifiedRecips === false) {
3115
-			$recipienttable = mapi_message_getrecipienttable($message);
3116
-			$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3117
-
3118
-			if ($basedate && empty($modifiedRecips)) {
3119
-				// Retrieve full list
3120
-				$recipienttable = mapi_message_getrecipienttable($this->message);
3121
-				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops);
3122
-
3123
-				// Save recipients in exceptions
3124
-				mapi_message_modifyrecipients($message, MODRECIP_ADD, $modifiedRecips);
3125
-
3126
-				// Now retrieve only those recipient who should receive this meeting request.
3127
-				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3128
-			}
3129
-		}
3130
-
3131
-		// @TODO: handle nonAcceptingResources
3132
-		/*
2769
+        // Get resource recipients
2770
+        $getResourcesRestriction = [
2771
+            RES_AND,
2772
+            [
2773
+                [
2774
+                    RES_PROPERTY,
2775
+                    [
2776
+                        RELOP => RELOP_EQ,    // Equals recipient type 3: Resource
2777
+                        ULPROPTAG => PR_RECIPIENT_TYPE,
2778
+                        VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2779
+                    ],
2780
+                ], ],
2781
+        ];
2782
+        $recipienttable = mapi_message_getrecipienttable($message);
2783
+        $resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2784
+        if (!empty($resourceRecipients)) {
2785
+            // Set Tracking status of resource recipients to olResponseAccepted (3)
2786
+            for ($i = 0, $len = count($resourceRecipients); $i < $len; ++$i) {
2787
+                $resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted;
2788
+                $resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time();
2789
+            }
2790
+            mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $resourceRecipients);
2791
+        }
2792
+
2793
+        // Publish updated free/busy information
2794
+        if (!$this->errorSetResource) {
2795
+            for ($i = 0, $len = count($resourceRecipData); $i < $len; ++$i) {
2796
+                $storeProps = mapi_getprops($resourceRecipData[$i]['store'], [PR_MAILBOX_OWNER_ENTRYID]);
2797
+                if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID])) {
2798
+                    $start = time() - 7 * 24 * 60 * 60;
2799
+                    $range = strtotime("+6 month");
2800
+                    $range = $range - (7 * 24 * 60 * 60);
2801
+
2802
+                    $pub = new FreeBusyPublish($this->session, $resourceRecipData[$i]['store'], $resourceRecipData[$i]['folder'], $storeProps[PR_MAILBOX_OWNER_ENTRYID]);
2803
+                    $pub->publishFB($start, $range); // publish from one week ago, 6 months ahead
2804
+                }
2805
+            }
2806
+        }
2807
+
2808
+        return $resourceRecipData;
2809
+    }
2810
+
2811
+    /**
2812
+     * Function which save an exception into recurring item.
2813
+     *
2814
+     * @param resource $recurringItem  reference to MAPI_message of recurring item
2815
+     * @param resource $occurrenceItem reference to MAPI_message of occurrence
2816
+     * @param string   $basedate       basedate of occurrence
2817
+     * @param bool     $move           if true then occurrence item is deleted
2818
+     * @param bool     $tentative      true if user has tentatively accepted it or false if user has accepted it
2819
+     * @param bool     $userAction     true if user has manually responded to meeting request
2820
+     * @param resource $store          user store
2821
+     * @param bool     $isDelegate     true if delegate is processing this meeting request
2822
+     */
2823
+    public function acceptException(&$recurringItem, &$occurrenceItem, $basedate, $move = false, $tentative, $userAction = false, $store, $isDelegate = false) {
2824
+        $recurr = new Recurrence($store, $recurringItem);
2825
+
2826
+        // Copy properties from meeting request
2827
+        $exception_props = mapi_getprops($occurrenceItem);
2828
+
2829
+        // Copy recipients list
2830
+        $reciptable = mapi_message_getrecipienttable($occurrenceItem);
2831
+        // If delegate, then do not add the delegate in recipients
2832
+        if ($isDelegate) {
2833
+            $delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2834
+            $res = [RES_PROPERTY, [
2835
+                RELOP => RELOP_NE,
2836
+                ULPROPTAG => PR_EMAIL_ADDRESS,
2837
+                VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2838
+            ],
2839
+            ];
2840
+            $recips = mapi_table_queryallrows($reciptable, $this->recipprops, $res);
2841
+        }
2842
+        else {
2843
+            $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2844
+        }
2845
+
2846
+        // add owner to recipient table
2847
+        $this->addOrganizer($exception_props, $recips, true);
2848
+
2849
+        // add delegator to meetings
2850
+        if ($isDelegate) {
2851
+            $this->addDelegator($exception_props, $recips);
2852
+        }
2853
+
2854
+        $exception_props[$this->proptags['meetingstatus']] = olMeetingReceived;
2855
+        $exception_props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
2856
+
2857
+        if (isset($exception_props[$this->proptags['intendedbusystatus']])) {
2858
+            if ($tentative && $exception_props[$this->proptags['intendedbusystatus']] !== fbFree) {
2859
+                $exception_props[$this->proptags['busystatus']] = fbTentative;
2860
+            }
2861
+            else {
2862
+                $exception_props[$this->proptags['busystatus']] = $exception_props[$this->proptags['intendedbusystatus']];
2863
+            }
2864
+            // we already have intendedbusystatus value in $exception_props so no need to copy it
2865
+        }
2866
+        else {
2867
+            $exception_props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
2868
+        }
2869
+
2870
+        if ($userAction) {
2871
+            $addrInfo = $this->getOwnerAddress($this->store);
2872
+
2873
+            // if user has responded then set replytime and name
2874
+            $exception_props[$this->proptags['replytime']] = time();
2875
+            if (!empty($addrInfo)) {
2876
+                $exception_props[$this->proptags['apptreplyname']] = $addrInfo[0];
2877
+            }
2878
+        }
2879
+
2880
+        if ($recurr->isException($basedate)) {
2881
+            $recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2882
+        }
2883
+        else {
2884
+            $recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2885
+        }
2886
+
2887
+        // Move the occurrenceItem to the waste basket
2888
+        if ($move) {
2889
+            $wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2890
+            $sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
2891
+            mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
2892
+        }
2893
+
2894
+        mapi_savechanges($recurringItem);
2895
+    }
2896
+
2897
+    /**
2898
+     * Function which merges an exception mapi message to recurring message.
2899
+     * This will be used when we receive recurring meeting request and we already have an exception message
2900
+     * of same meeting in calendar and we need to remove that exception message and add it to attachment table
2901
+     * of recurring meeting.
2902
+     *
2903
+     * @param resource $recurringItem  reference to MAPI_message of recurring item
2904
+     * @param resource $occurrenceItem reference to MAPI_message of occurrence
2905
+     * @param string   $basedate       basedate of occurrence
2906
+     * @param resource $store          user store
2907
+     */
2908
+    public function mergeException(&$recurringItem, &$occurrenceItem, $basedate, $store) {
2909
+        $recurr = new Recurrence($store, $recurringItem);
2910
+
2911
+        // Copy properties from meeting request
2912
+        $exception_props = mapi_getprops($occurrenceItem);
2913
+
2914
+        // Get recipient list from message and add it to exception attachment
2915
+        $reciptable = mapi_message_getrecipienttable($occurrenceItem);
2916
+        $recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2917
+
2918
+        if ($recurr->isException($basedate)) {
2919
+            $recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2920
+        }
2921
+        else {
2922
+            $recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2923
+        }
2924
+
2925
+        // Move the occurrenceItem to the waste basket
2926
+        $wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2927
+        $sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
2928
+        mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
2929
+
2930
+        mapi_savechanges($recurringItem);
2931
+    }
2932
+
2933
+    /**
2934
+     * Function which submits meeting request based on arguments passed to it.
2935
+     *
2936
+     * @param resource $message        MAPI_message whose meeting request is to be send
2937
+     * @param bool     $cancel         if true send request, else send cancellation
2938
+     * @param string   $prefix         subject prefix
2939
+     * @param int      $basedate       basedate for an occurrence
2940
+     * @param object   $recurObject    recurrence object of mr
2941
+     * @param bool     $copyExceptions When sending update mail for recurring item then we dont send exceptions in attachments
2942
+     * @param mixed    $modifiedRecips
2943
+     * @param mixed    $deletedRecips
2944
+     */
2945
+    public function submitMeetingRequest($message, $cancel, $prefix, $basedate = false, $recurObject = false, $copyExceptions = true, $modifiedRecips = false, $deletedRecips = false) {
2946
+        $newmessageprops = $messageprops = mapi_getprops($this->message);
2947
+        $new = $this->createOutgoingMessage();
2948
+
2949
+        // Copy the entire message into the new meeting request message
2950
+        if ($basedate) {
2951
+            // messageprops contains properties of whole recurring series
2952
+            // and newmessageprops contains properties of exception item
2953
+            $newmessageprops = mapi_getprops($message);
2954
+
2955
+            // Ensure that the correct basedate is set in the new message
2956
+            $newmessageprops[$this->proptags['basedate']] = $basedate;
2957
+
2958
+            // Set isRecurring to false, because this is an exception
2959
+            $newmessageprops[$this->proptags['recurring']] = false;
2960
+
2961
+            // set LID_IS_EXCEPTION to true
2962
+            $newmessageprops[$this->proptags['is_exception']] = true;
2963
+
2964
+            // Set to high importance
2965
+            if ($cancel) {
2966
+                $newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;
2967
+            }
2968
+
2969
+            // Set startdate and enddate of exception
2970
+            if ($cancel && $recurObject) {
2971
+                $newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
2972
+                $newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
2973
+            }
2974
+
2975
+            // Set basedate in guid (0x3)
2976
+            $newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
2977
+            $newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
2978
+            $newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
2979
+
2980
+            // Get deleted recipiets from exception msg
2981
+            $restriction = [
2982
+                RES_AND,
2983
+                [
2984
+                    [
2985
+                        RES_BITMASK,
2986
+                        [
2987
+                            ULTYPE => BMR_NEZ,
2988
+                            ULPROPTAG => PR_RECIPIENT_FLAGS,
2989
+                            ULMASK => recipExceptionalDeleted,
2990
+                        ],
2991
+                    ],
2992
+                    [
2993
+                        RES_BITMASK,
2994
+                        [
2995
+                            ULTYPE => BMR_EQZ,
2996
+                            ULPROPTAG => PR_RECIPIENT_FLAGS,
2997
+                            ULMASK => recipOrganizer,
2998
+                        ],
2999
+                    ],
3000
+                ],
3001
+            ];
3002
+
3003
+            // In direct-booking mode, we don't need to send cancellations to resources
3004
+            if ($this->enableDirectBooking) {
3005
+                $restriction[1][] = [
3006
+                    RES_PROPERTY,
3007
+                    [
3008
+                        RELOP => RELOP_NE,    // Does not equal recipient type: MAPI_BCC (Resource)
3009
+                        ULPROPTAG => PR_RECIPIENT_TYPE,
3010
+                        VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3011
+                    ],
3012
+                ];
3013
+            }
3014
+
3015
+            $recipienttable = mapi_message_getrecipienttable($message);
3016
+            $recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $restriction);
3017
+
3018
+            if (!$deletedRecips) {
3019
+                $deletedRecips = array_merge([], $recipients);
3020
+            }
3021
+            else {
3022
+                $deletedRecips = array_merge($deletedRecips, $recipients);
3023
+            }
3024
+        }
3025
+
3026
+        // Remove the PR_ICON_INDEX as it is not needed in the sent message.
3027
+        $newmessageprops[PR_ICON_INDEX] = null;
3028
+        $newmessageprops[PR_RESPONSE_REQUESTED] = true;
3029
+
3030
+        // PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3031
+        $newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']];
3032
+        $newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']];
3033
+
3034
+        // Set updatecounter/AppointmentSequenceNumber
3035
+        // get the value of latest updatecounter for the whole series and use it
3036
+        $newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']];
3037
+
3038
+        $meetingTimeInfo = $this->getMeetingTimeInfo();
3039
+
3040
+        if ($meetingTimeInfo) {
3041
+            $newmessageprops[PR_BODY] = $meetingTimeInfo;
3042
+        }
3043
+
3044
+        // Send all recurrence info in mail, if this is a recurrence meeting.
3045
+        if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']]) {
3046
+            if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
3047
+                $newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
3048
+            }
3049
+            $newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
3050
+            $newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
3051
+            $newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
3052
+
3053
+            if ($recurObject) {
3054
+                $this->generateRecurDates($recurObject, $messageprops, $newmessageprops);
3055
+            }
3056
+        }
3057
+
3058
+        if (isset($newmessageprops[$this->proptags['counter_proposal']])) {
3059
+            unset($newmessageprops[$this->proptags['counter_proposal']]);
3060
+        }
3061
+
3062
+        // Prefix the subject if needed
3063
+        if ($prefix && isset($newmessageprops[PR_SUBJECT])) {
3064
+            $newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT];
3065
+        }
3066
+
3067
+        if (isset($newmessageprops[$this->proptags['categories']]) &&
3068
+            !empty($newmessageprops[$this->proptags['categories']])) {
3069
+            unset($newmessageprops[$this->proptags['categories']]);
3070
+        }
3071
+        mapi_setprops($new, $newmessageprops);
3072
+
3073
+        // Copy attachments
3074
+        $this->replaceAttachments($message, $new, $copyExceptions);
3075
+
3076
+        // Retrieve only those recipient who should receive this meeting request.
3077
+        $stripResourcesRestriction = [
3078
+            RES_AND,
3079
+            [
3080
+                [
3081
+                    RES_BITMASK,
3082
+                    [
3083
+                        ULTYPE => BMR_EQZ,
3084
+                        ULPROPTAG => PR_RECIPIENT_FLAGS,
3085
+                        ULMASK => recipExceptionalDeleted,
3086
+                    ],
3087
+                ],
3088
+                [
3089
+                    RES_BITMASK,
3090
+                    [
3091
+                        ULTYPE => BMR_EQZ,
3092
+                        ULPROPTAG => PR_RECIPIENT_FLAGS,
3093
+                        ULMASK => recipOrganizer,
3094
+                    ],
3095
+                ],
3096
+            ],
3097
+        ];
3098
+
3099
+        // In direct-booking mode, resources do not receive a meeting request
3100
+        if ($this->enableDirectBooking) {
3101
+            $stripResourcesRestriction[1][] =
3102
+                                    [
3103
+                                        RES_PROPERTY,
3104
+                                        [
3105
+                                            RELOP => RELOP_NE,    // Does not equal recipient type: MAPI_BCC (Resource)
3106
+                                            ULPROPTAG => PR_RECIPIENT_TYPE,
3107
+                                            VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3108
+                                        ],
3109
+                                    ];
3110
+        }
3111
+
3112
+        // If no recipients were explicitly provided, we will send the update to all
3113
+        // recipients from the meeting.
3114
+        if ($modifiedRecips === false) {
3115
+            $recipienttable = mapi_message_getrecipienttable($message);
3116
+            $modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3117
+
3118
+            if ($basedate && empty($modifiedRecips)) {
3119
+                // Retrieve full list
3120
+                $recipienttable = mapi_message_getrecipienttable($this->message);
3121
+                $modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops);
3122
+
3123
+                // Save recipients in exceptions
3124
+                mapi_message_modifyrecipients($message, MODRECIP_ADD, $modifiedRecips);
3125
+
3126
+                // Now retrieve only those recipient who should receive this meeting request.
3127
+                $modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3128
+            }
3129
+        }
3130
+
3131
+        // @TODO: handle nonAcceptingResources
3132
+        /*
3133 3133
 		 * Add resource recipients that did not automatically accept the meeting request.
3134 3134
 		 * (note: meaning that they did not decline the meeting request)
3135 3135
 		 */ /*
@@ -3137,803 +3137,803 @@  discard block
 block discarded – undo
3137 3137
 			$recipients[] = $this->nonAcceptingResources[$i];
3138 3138
 		}*/
3139 3139
 
3140
-		if (!empty($modifiedRecips)) {
3141
-			// Strip out the sender/'owner' recipient
3142
-			mapi_message_modifyrecipients($new, MODRECIP_ADD, $modifiedRecips);
3143
-
3144
-			// Set some properties that are different in the sent request than
3145
-			// in the item in our calendar
3146
-
3147
-			// we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request
3148
-			// should always be fbTentative
3149
-			$newmessageprops[$this->proptags['intendedbusystatus']] = isset($newmessageprops[$this->proptags['busystatus']]) ? $newmessageprops[$this->proptags['busystatus']] : $messageprops[$this->proptags['busystatus']];
3150
-			$newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted
3151
-			$newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet
3152
-			$newmessageprops[$this->proptags['attendee_critical_change']] = time();
3153
-			$newmessageprops[$this->proptags['owner_critical_change']] = time();
3154
-			$newmessageprops[$this->proptags['meetingtype']] = mtgRequest;
3155
-
3156
-			if ($cancel) {
3157
-				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3158
-				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3159
-				$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3160
-			}
3161
-			else {
3162
-				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Request';
3163
-				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
3164
-			}
3165
-
3166
-			mapi_setprops($new, $newmessageprops);
3167
-			mapi_savechanges($new);
3168
-
3169
-			// Submit message to non-resource recipients
3170
-			mapi_message_submitmessage($new);
3171
-		}
3172
-
3173
-		// Search through the deleted recipients, and see if any of them is also
3174
-		// listed as a recipient to whom we have send an update. As we don't
3175
-		// want to send a cancellation message to recipients who will also receive
3176
-		// an meeting update, we have to filter those recipients out.
3177
-		if ($deletedRecips) {
3178
-			$tmp = [];
3179
-
3180
-			foreach ($deletedRecips as $delRecip) {
3181
-				$found = false;
3182
-
3183
-				// Search if the deleted recipient can be found inside
3184
-				// the updated recipients as well.
3185
-				foreach ($modifiedRecips as $recip) {
3186
-					if ($this->compareABEntryIDs($recip[PR_ENTRYID], $delRecip[PR_ENTRYID])) {
3187
-						$found = true;
3188
-
3189
-						break;
3190
-					}
3191
-				}
3192
-
3193
-				// If the recipient was not found, it truly is deleted,
3194
-				// and we can safely send a cancellation message
3195
-				if (!$found) {
3196
-					$tmp[] = $delRecip;
3197
-				}
3198
-			}
3199
-
3200
-			$deletedRecips = $tmp;
3201
-		}
3202
-
3203
-		// Send cancellation to deleted attendees
3204
-		if ($deletedRecips && !empty($deletedRecips)) {
3205
-			$new = $this->createOutgoingMessage();
3206
-
3207
-			mapi_message_modifyrecipients($new, MODRECIP_ADD, $deletedRecips);
3208
-
3209
-			$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3210
-			$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3211
-			$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3212
-			$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;    // HIGH Importance
3213
-			if (isset($newmessageprops[PR_SUBJECT])) {
3214
-				$newmessageprops[PR_SUBJECT] = _('Canceled: ') . $newmessageprops[PR_SUBJECT];
3215
-			}
3216
-
3217
-			mapi_setprops($new, $newmessageprops);
3218
-			mapi_savechanges($new);
3219
-
3220
-			// Submit message to non-resource recipients
3221
-			mapi_message_submitmessage($new);
3222
-		}
3223
-
3224
-		// Set properties on meeting object in calendar
3225
-		// Set requestsent to 'true' (turns on 'tracking', etc)
3226
-		$props = [];
3227
-		$props[$this->proptags['meetingstatus']] = olMeeting;
3228
-		$props[$this->proptags['responsestatus']] = olResponseOrganized;
3229
-		// Only set the 'requestsent' property if it wasn't set previously yet,
3230
-		// this ensures we will not accidentally set it from true to false.
3231
-		if (!isset($messageprops[$this->proptags['requestsent']]) || $messageprops[$this->proptags['requestsent']] !== true) {
3232
-			$props[$this->proptags['requestsent']] = !empty($modifiedRecips) || ($this->includesResources && !$this->errorSetResource);
3233
-		}
3234
-		$props[$this->proptags['attendee_critical_change']] = time();
3235
-		$props[$this->proptags['owner_critical_change']] = time();
3236
-		$props[$this->proptags['meetingtype']] = mtgRequest;
3237
-		// save the new updatecounter to exception/recurring series/normal meeting
3238
-		$props[$this->proptags['updatecounter']] = $newmessageprops[$this->proptags['updatecounter']];
3239
-
3240
-		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3241
-		$props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
3242
-		$props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
3243
-
3244
-		mapi_setprops($message, $props);
3245
-
3246
-		// saving of these properties on calendar item should be handled by caller function
3247
-		// based on sending meeting request was successful or not
3248
-	}
3249
-
3250
-	/**
3251
-	 * OL2007 uses these 4 properties to specify occurrence that should be updated.
3252
-	 * ical generates RECURRENCE-ID property based on exception's basedate (PidLidExceptionReplaceTime),
3253
-	 * but OL07 doesn't send this property, so ical will generate RECURRENCE-ID property based on date
3254
-	 * from GlobalObjId and time from StartRecurTime property, so we are sending basedate property and
3255
-	 * also additionally we are sending these properties.
3256
-	 * Ref: MS-OXCICAL 2.2.1.20.20 Property: RECURRENCE-ID.
3257
-	 *
3258
-	 * @param object $recurObject     instance of recurrence class for this message
3259
-	 * @param array  $messageprops    properties of meeting object that is going to be send
3260
-	 * @param array  $newmessageprops properties of meeting request/response that is going to be send
3261
-	 */
3262
-	public function generateRecurDates($recurObject, $messageprops, &$newmessageprops) {
3263
-		if ($messageprops[$this->proptags['startdate']] && $messageprops[$this->proptags['duedate']]) {
3264
-			$startDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['startdate']]));
3265
-			$endDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['duedate']]));
3266
-
3267
-			$startDate = explode(':', $startDate);
3268
-			$endDate = explode(':', $endDate);
3269
-
3270
-			// [0] => year, [1] => month, [2] => day, [3] => hour, [4] => minutes, [5] => seconds
3271
-			// RecurStartDate = year * 512 + month_number * 32 + day_number
3272
-			$newmessageprops[$this->proptags['start_recur_date']] = (((int) $startDate[0]) * 512) + (((int) $startDate[1]) * 32) + ((int) $startDate[2]);
3273
-			// RecurStartTime = hour * 4096 + minutes * 64 + seconds
3274
-			$newmessageprops[$this->proptags['start_recur_time']] = (((int) $startDate[3]) * 4096) + (((int) $startDate[4]) * 64) + ((int) $startDate[5]);
3275
-
3276
-			$newmessageprops[$this->proptags['end_recur_date']] = (((int) $endDate[0]) * 512) + (((int) $endDate[1]) * 32) + ((int) $endDate[2]);
3277
-			$newmessageprops[$this->proptags['end_recur_time']] = (((int) $endDate[3]) * 4096) + (((int) $endDate[4]) * 64) + ((int) $endDate[5]);
3278
-		}
3279
-	}
3280
-
3281
-	/**
3282
-	 * Function will create a new outgoing message that will be used to send meeting mail.
3283
-	 *
3284
-	 * @param MAPIStore $store (optional) store that is used when creating response, if delegate is creating outgoing mail
3285
-	 *                         then this would point to delegate store
3286
-	 *
3287
-	 * @return MAPIMessage outgoing mail that is created and can be used for sending it
3288
-	 */
3289
-	public function createOutgoingMessage($store = false) {
3290
-		// get logged in user's store that will be used to send mail, for delegate this will be
3291
-		// delegate store
3292
-		$userStore = $this->openDefaultStore();
3293
-
3294
-		$sentprops = [];
3295
-		$outbox = $this->openDefaultOutbox($userStore);
3296
-
3297
-		$outgoing = mapi_folder_createmessage($outbox);
3298
-
3299
-		// check if $store is set and it is not equal to $defaultStore (means its the delegation case)
3300
-		if ($store !== false) {
3301
-			$storeProps = mapi_getprops($store, [PR_ENTRYID]);
3302
-			$userStoreProps = mapi_getprops($userStore, [PR_ENTRYID]);
3303
-
3304
-			// @FIXME use entryid comparison functions here
3305
-			if ($storeProps[PR_ENTRYID] !== $userStoreProps[PR_ENTRYID]) {
3306
-				// get the delegator properties and set it into outgoing mail
3307
-				$delegatorDetails = $this->getOwnerAddress($store, false);
3308
-
3309
-				if ($delegatorDetails) {
3310
-					list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegatorDetails;
3311
-					$sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3312
-					$sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3313
-					$sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3314
-					$sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3315
-					$sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3316
-				}
3317
-
3318
-				// get the delegate properties and set it into outgoing mail
3319
-				$delegateDetails = $this->getOwnerAddress($userStore, false);
3320
-
3321
-				if ($delegateDetails) {
3322
-					list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegateDetails;
3323
-					$sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3324
-					$sentprops[PR_SENDER_NAME] = $ownername;
3325
-					$sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3326
-					$sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3327
-					$sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3328
-				}
3329
-			}
3330
-		}
3331
-		else {
3332
-			// normal user is sending mail, so both set of properties will be same
3333
-			$userDetails = $this->getOwnerAddress($userStore);
3334
-
3335
-			if ($userDetails) {
3336
-				list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $userDetails;
3337
-				$sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3338
-				$sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3339
-				$sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3340
-				$sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3341
-				$sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3342
-
3343
-				$sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3344
-				$sentprops[PR_SENDER_NAME] = $ownername;
3345
-				$sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3346
-				$sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3347
-				$sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3348
-			}
3349
-		}
3350
-
3351
-		$sentprops[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($userStore);
3352
-
3353
-		mapi_setprops($outgoing, $sentprops);
3354
-
3355
-		return $outgoing;
3356
-	}
3357
-
3358
-	/**
3359
-	 * Function which checks that meeting in attendee's calendar is already updated
3360
-	 * and we are checking an old meeting request. This function also will update property
3361
-	 * meetingtype to indicate that its out of date meeting request.
3362
-	 *
3363
-	 * @return bool true if meeting request is outofdate else false if it is new
3364
-	 */
3365
-	public function isMeetingOutOfDate() {
3366
-		$result = false;
3367
-
3368
-		$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']]);
3369
-
3370
-		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS])) {
3371
-			return $result;
3372
-		}
3373
-
3374
-		if (isset($props[$this->proptags['meetingtype']]) && ($props[$this->proptags['meetingtype']] & mtgOutOfDate) == mtgOutOfDate) {
3375
-			return true;
3376
-		}
3377
-
3378
-		// get the basedate to check for exception
3379
-		$basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]);
3380
-
3381
-		$calendarItem = $this->getCorrespondentCalendarItem(true);
3382
-
3383
-		// if basedate is provided and we could not find the item then it could be that we are checking
3384
-		// an exception so get the exception and check it
3385
-		if ($basedate && $calendarItem !== false) {
3386
-			$exception = $this->getExceptionItem($calendarItem, $basedate);
3387
-
3388
-			if ($exception !== false) {
3389
-				// we are able to find the exception compare with it
3390
-				$calendarItem = $exception;
3391
-			}
3392
-			// we are not able to find exception, could mean that a significant change has occurred on series
3393
-				// and it deleted all exceptions, so compare with series
3394
-				// $calendarItem already contains reference to series
3395
-		}
3396
-
3397
-		if ($calendarItem !== false) {
3398
-			$calendarItemProps = mapi_getprops($calendarItem, [
3399
-				$this->proptags['owner_critical_change'],
3400
-				$this->proptags['updatecounter'],
3401
-			]);
3402
-
3403
-			$updateCounter = (isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]);
3404
-
3405
-			$criticalChange = (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']]);
3406
-
3407
-			if ($updateCounter || $criticalChange) {
3408
-				// meeting request is out of date, set properties to indicate this
3409
-				mapi_setprops($this->message, [$this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033]);
3410
-				mapi_savechanges($this->message);
3411
-
3412
-				$result = true;
3413
-			}
3414
-		}
3415
-
3416
-		return $result;
3417
-	}
3418
-
3419
-	/**
3420
-	 * Function which checks that if we have received a meeting response for an updated meeting in organizer's calendar.
3421
-	 *
3422
-	 * @param Number $basedate basedate of the exception if we want to compare with exception
3423
-	 *
3424
-	 * @return bool true if meeting request is updated later
3425
-	 */
3426
-	public function isMeetingUpdated($basedate = false) {
3427
-		$result = false;
3428
-
3429
-		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['updatecounter']]);
3430
-
3431
-		if (!$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS])) {
3432
-			return $result;
3433
-		}
3434
-
3435
-		$calendarItem = $this->getCorrespondentCalendarItem(true);
3436
-
3437
-		if ($calendarItem !== false) {
3438
-			// basedate is provided so open exception
3439
-			if ($basedate !== false) {
3440
-				$exception = $this->getExceptionItem($calendarItem, $basedate);
3441
-
3442
-				if ($exception !== false) {
3443
-					// we are able to find the exception compare with it
3444
-					$calendarItem = $exception;
3445
-				}
3446
-				// we are not able to find exception, could mean that a significant change has occurred on series
3447
-					// and it deleted all exceptions, so compare with series
3448
-					// $calendarItem already contains reference to series
3449
-			}
3450
-
3451
-			if ($calendarItem !== false) {
3452
-				$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['updatecounter']]);
3453
-
3454
-				/*
3140
+        if (!empty($modifiedRecips)) {
3141
+            // Strip out the sender/'owner' recipient
3142
+            mapi_message_modifyrecipients($new, MODRECIP_ADD, $modifiedRecips);
3143
+
3144
+            // Set some properties that are different in the sent request than
3145
+            // in the item in our calendar
3146
+
3147
+            // we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request
3148
+            // should always be fbTentative
3149
+            $newmessageprops[$this->proptags['intendedbusystatus']] = isset($newmessageprops[$this->proptags['busystatus']]) ? $newmessageprops[$this->proptags['busystatus']] : $messageprops[$this->proptags['busystatus']];
3150
+            $newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted
3151
+            $newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet
3152
+            $newmessageprops[$this->proptags['attendee_critical_change']] = time();
3153
+            $newmessageprops[$this->proptags['owner_critical_change']] = time();
3154
+            $newmessageprops[$this->proptags['meetingtype']] = mtgRequest;
3155
+
3156
+            if ($cancel) {
3157
+                $newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3158
+                $newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3159
+                $newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3160
+            }
3161
+            else {
3162
+                $newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Request';
3163
+                $newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
3164
+            }
3165
+
3166
+            mapi_setprops($new, $newmessageprops);
3167
+            mapi_savechanges($new);
3168
+
3169
+            // Submit message to non-resource recipients
3170
+            mapi_message_submitmessage($new);
3171
+        }
3172
+
3173
+        // Search through the deleted recipients, and see if any of them is also
3174
+        // listed as a recipient to whom we have send an update. As we don't
3175
+        // want to send a cancellation message to recipients who will also receive
3176
+        // an meeting update, we have to filter those recipients out.
3177
+        if ($deletedRecips) {
3178
+            $tmp = [];
3179
+
3180
+            foreach ($deletedRecips as $delRecip) {
3181
+                $found = false;
3182
+
3183
+                // Search if the deleted recipient can be found inside
3184
+                // the updated recipients as well.
3185
+                foreach ($modifiedRecips as $recip) {
3186
+                    if ($this->compareABEntryIDs($recip[PR_ENTRYID], $delRecip[PR_ENTRYID])) {
3187
+                        $found = true;
3188
+
3189
+                        break;
3190
+                    }
3191
+                }
3192
+
3193
+                // If the recipient was not found, it truly is deleted,
3194
+                // and we can safely send a cancellation message
3195
+                if (!$found) {
3196
+                    $tmp[] = $delRecip;
3197
+                }
3198
+            }
3199
+
3200
+            $deletedRecips = $tmp;
3201
+        }
3202
+
3203
+        // Send cancellation to deleted attendees
3204
+        if ($deletedRecips && !empty($deletedRecips)) {
3205
+            $new = $this->createOutgoingMessage();
3206
+
3207
+            mapi_message_modifyrecipients($new, MODRECIP_ADD, $deletedRecips);
3208
+
3209
+            $newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3210
+            $newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3211
+            $newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3212
+            $newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;    // HIGH Importance
3213
+            if (isset($newmessageprops[PR_SUBJECT])) {
3214
+                $newmessageprops[PR_SUBJECT] = _('Canceled: ') . $newmessageprops[PR_SUBJECT];
3215
+            }
3216
+
3217
+            mapi_setprops($new, $newmessageprops);
3218
+            mapi_savechanges($new);
3219
+
3220
+            // Submit message to non-resource recipients
3221
+            mapi_message_submitmessage($new);
3222
+        }
3223
+
3224
+        // Set properties on meeting object in calendar
3225
+        // Set requestsent to 'true' (turns on 'tracking', etc)
3226
+        $props = [];
3227
+        $props[$this->proptags['meetingstatus']] = olMeeting;
3228
+        $props[$this->proptags['responsestatus']] = olResponseOrganized;
3229
+        // Only set the 'requestsent' property if it wasn't set previously yet,
3230
+        // this ensures we will not accidentally set it from true to false.
3231
+        if (!isset($messageprops[$this->proptags['requestsent']]) || $messageprops[$this->proptags['requestsent']] !== true) {
3232
+            $props[$this->proptags['requestsent']] = !empty($modifiedRecips) || ($this->includesResources && !$this->errorSetResource);
3233
+        }
3234
+        $props[$this->proptags['attendee_critical_change']] = time();
3235
+        $props[$this->proptags['owner_critical_change']] = time();
3236
+        $props[$this->proptags['meetingtype']] = mtgRequest;
3237
+        // save the new updatecounter to exception/recurring series/normal meeting
3238
+        $props[$this->proptags['updatecounter']] = $newmessageprops[$this->proptags['updatecounter']];
3239
+
3240
+        // PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3241
+        $props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
3242
+        $props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
3243
+
3244
+        mapi_setprops($message, $props);
3245
+
3246
+        // saving of these properties on calendar item should be handled by caller function
3247
+        // based on sending meeting request was successful or not
3248
+    }
3249
+
3250
+    /**
3251
+     * OL2007 uses these 4 properties to specify occurrence that should be updated.
3252
+     * ical generates RECURRENCE-ID property based on exception's basedate (PidLidExceptionReplaceTime),
3253
+     * but OL07 doesn't send this property, so ical will generate RECURRENCE-ID property based on date
3254
+     * from GlobalObjId and time from StartRecurTime property, so we are sending basedate property and
3255
+     * also additionally we are sending these properties.
3256
+     * Ref: MS-OXCICAL 2.2.1.20.20 Property: RECURRENCE-ID.
3257
+     *
3258
+     * @param object $recurObject     instance of recurrence class for this message
3259
+     * @param array  $messageprops    properties of meeting object that is going to be send
3260
+     * @param array  $newmessageprops properties of meeting request/response that is going to be send
3261
+     */
3262
+    public function generateRecurDates($recurObject, $messageprops, &$newmessageprops) {
3263
+        if ($messageprops[$this->proptags['startdate']] && $messageprops[$this->proptags['duedate']]) {
3264
+            $startDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['startdate']]));
3265
+            $endDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['duedate']]));
3266
+
3267
+            $startDate = explode(':', $startDate);
3268
+            $endDate = explode(':', $endDate);
3269
+
3270
+            // [0] => year, [1] => month, [2] => day, [3] => hour, [4] => minutes, [5] => seconds
3271
+            // RecurStartDate = year * 512 + month_number * 32 + day_number
3272
+            $newmessageprops[$this->proptags['start_recur_date']] = (((int) $startDate[0]) * 512) + (((int) $startDate[1]) * 32) + ((int) $startDate[2]);
3273
+            // RecurStartTime = hour * 4096 + minutes * 64 + seconds
3274
+            $newmessageprops[$this->proptags['start_recur_time']] = (((int) $startDate[3]) * 4096) + (((int) $startDate[4]) * 64) + ((int) $startDate[5]);
3275
+
3276
+            $newmessageprops[$this->proptags['end_recur_date']] = (((int) $endDate[0]) * 512) + (((int) $endDate[1]) * 32) + ((int) $endDate[2]);
3277
+            $newmessageprops[$this->proptags['end_recur_time']] = (((int) $endDate[3]) * 4096) + (((int) $endDate[4]) * 64) + ((int) $endDate[5]);
3278
+        }
3279
+    }
3280
+
3281
+    /**
3282
+     * Function will create a new outgoing message that will be used to send meeting mail.
3283
+     *
3284
+     * @param MAPIStore $store (optional) store that is used when creating response, if delegate is creating outgoing mail
3285
+     *                         then this would point to delegate store
3286
+     *
3287
+     * @return MAPIMessage outgoing mail that is created and can be used for sending it
3288
+     */
3289
+    public function createOutgoingMessage($store = false) {
3290
+        // get logged in user's store that will be used to send mail, for delegate this will be
3291
+        // delegate store
3292
+        $userStore = $this->openDefaultStore();
3293
+
3294
+        $sentprops = [];
3295
+        $outbox = $this->openDefaultOutbox($userStore);
3296
+
3297
+        $outgoing = mapi_folder_createmessage($outbox);
3298
+
3299
+        // check if $store is set and it is not equal to $defaultStore (means its the delegation case)
3300
+        if ($store !== false) {
3301
+            $storeProps = mapi_getprops($store, [PR_ENTRYID]);
3302
+            $userStoreProps = mapi_getprops($userStore, [PR_ENTRYID]);
3303
+
3304
+            // @FIXME use entryid comparison functions here
3305
+            if ($storeProps[PR_ENTRYID] !== $userStoreProps[PR_ENTRYID]) {
3306
+                // get the delegator properties and set it into outgoing mail
3307
+                $delegatorDetails = $this->getOwnerAddress($store, false);
3308
+
3309
+                if ($delegatorDetails) {
3310
+                    list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegatorDetails;
3311
+                    $sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3312
+                    $sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3313
+                    $sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3314
+                    $sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3315
+                    $sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3316
+                }
3317
+
3318
+                // get the delegate properties and set it into outgoing mail
3319
+                $delegateDetails = $this->getOwnerAddress($userStore, false);
3320
+
3321
+                if ($delegateDetails) {
3322
+                    list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegateDetails;
3323
+                    $sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3324
+                    $sentprops[PR_SENDER_NAME] = $ownername;
3325
+                    $sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3326
+                    $sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3327
+                    $sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3328
+                }
3329
+            }
3330
+        }
3331
+        else {
3332
+            // normal user is sending mail, so both set of properties will be same
3333
+            $userDetails = $this->getOwnerAddress($userStore);
3334
+
3335
+            if ($userDetails) {
3336
+                list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $userDetails;
3337
+                $sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3338
+                $sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3339
+                $sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3340
+                $sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3341
+                $sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3342
+
3343
+                $sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3344
+                $sentprops[PR_SENDER_NAME] = $ownername;
3345
+                $sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3346
+                $sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3347
+                $sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3348
+            }
3349
+        }
3350
+
3351
+        $sentprops[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($userStore);
3352
+
3353
+        mapi_setprops($outgoing, $sentprops);
3354
+
3355
+        return $outgoing;
3356
+    }
3357
+
3358
+    /**
3359
+     * Function which checks that meeting in attendee's calendar is already updated
3360
+     * and we are checking an old meeting request. This function also will update property
3361
+     * meetingtype to indicate that its out of date meeting request.
3362
+     *
3363
+     * @return bool true if meeting request is outofdate else false if it is new
3364
+     */
3365
+    public function isMeetingOutOfDate() {
3366
+        $result = false;
3367
+
3368
+        $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']]);
3369
+
3370
+        if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS])) {
3371
+            return $result;
3372
+        }
3373
+
3374
+        if (isset($props[$this->proptags['meetingtype']]) && ($props[$this->proptags['meetingtype']] & mtgOutOfDate) == mtgOutOfDate) {
3375
+            return true;
3376
+        }
3377
+
3378
+        // get the basedate to check for exception
3379
+        $basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]);
3380
+
3381
+        $calendarItem = $this->getCorrespondentCalendarItem(true);
3382
+
3383
+        // if basedate is provided and we could not find the item then it could be that we are checking
3384
+        // an exception so get the exception and check it
3385
+        if ($basedate && $calendarItem !== false) {
3386
+            $exception = $this->getExceptionItem($calendarItem, $basedate);
3387
+
3388
+            if ($exception !== false) {
3389
+                // we are able to find the exception compare with it
3390
+                $calendarItem = $exception;
3391
+            }
3392
+            // we are not able to find exception, could mean that a significant change has occurred on series
3393
+                // and it deleted all exceptions, so compare with series
3394
+                // $calendarItem already contains reference to series
3395
+        }
3396
+
3397
+        if ($calendarItem !== false) {
3398
+            $calendarItemProps = mapi_getprops($calendarItem, [
3399
+                $this->proptags['owner_critical_change'],
3400
+                $this->proptags['updatecounter'],
3401
+            ]);
3402
+
3403
+            $updateCounter = (isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]);
3404
+
3405
+            $criticalChange = (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']]);
3406
+
3407
+            if ($updateCounter || $criticalChange) {
3408
+                // meeting request is out of date, set properties to indicate this
3409
+                mapi_setprops($this->message, [$this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033]);
3410
+                mapi_savechanges($this->message);
3411
+
3412
+                $result = true;
3413
+            }
3414
+        }
3415
+
3416
+        return $result;
3417
+    }
3418
+
3419
+    /**
3420
+     * Function which checks that if we have received a meeting response for an updated meeting in organizer's calendar.
3421
+     *
3422
+     * @param Number $basedate basedate of the exception if we want to compare with exception
3423
+     *
3424
+     * @return bool true if meeting request is updated later
3425
+     */
3426
+    public function isMeetingUpdated($basedate = false) {
3427
+        $result = false;
3428
+
3429
+        $props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['updatecounter']]);
3430
+
3431
+        if (!$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS])) {
3432
+            return $result;
3433
+        }
3434
+
3435
+        $calendarItem = $this->getCorrespondentCalendarItem(true);
3436
+
3437
+        if ($calendarItem !== false) {
3438
+            // basedate is provided so open exception
3439
+            if ($basedate !== false) {
3440
+                $exception = $this->getExceptionItem($calendarItem, $basedate);
3441
+
3442
+                if ($exception !== false) {
3443
+                    // we are able to find the exception compare with it
3444
+                    $calendarItem = $exception;
3445
+                }
3446
+                // we are not able to find exception, could mean that a significant change has occurred on series
3447
+                    // and it deleted all exceptions, so compare with series
3448
+                    // $calendarItem already contains reference to series
3449
+            }
3450
+
3451
+            if ($calendarItem !== false) {
3452
+                $calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['updatecounter']]);
3453
+
3454
+                /*
3455 3455
 				 * if(message_counter < appointment_counter) meeting object is newer then meeting response (meeting is updated)
3456 3456
 				 * if(message_counter >= appointment_counter) meeting is not updated, do normal processing
3457 3457
 				 */
3458
-				if (isset($calendarItemProps[$this->proptags['updatecounter']], $props[$this->proptags['updatecounter']])) {
3459
-					if ($props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) {
3460
-						$result = true;
3461
-					}
3462
-				}
3463
-			}
3464
-		}
3465
-
3466
-		return $result;
3467
-	}
3468
-
3469
-	/**
3470
-	 * Checks if there has been any significant changes on appointment/meeting item.
3471
-	 * Significant changes be:
3472
-	 * 1) startdate has been changed
3473
-	 * 2) duedate has been changed OR
3474
-	 * 3) recurrence pattern has been created, modified or removed.
3475
-	 *
3476
-	 * @param array oldProps old props before an update
3477
-	 * @param Number basedate basedate
3478
-	 * @param bool isRecurrenceChanged for change in recurrence pattern.
3479
-	 * isRecurrenceChanged true means Recurrence pattern has been changed, so clear all attendees response
3480
-	 * @param mixed $oldProps
3481
-	 * @param mixed $basedate
3482
-	 * @param mixed $isRecurrenceChanged
3483
-	 */
3484
-	public function checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged = false) {
3485
-		$message = null;
3486
-		$attach = null;
3487
-
3488
-		// If basedate is specified then we need to open exception message to clear recipient responses
3489
-		if ($basedate) {
3490
-			$recurrence = new Recurrence($this->store, $this->message);
3491
-			if ($recurrence->isException($basedate)) {
3492
-				$attach = $recurrence->getExceptionAttachment($basedate);
3493
-				if ($attach) {
3494
-					$message = mapi_attach_openobj($attach, MAPI_MODIFY);
3495
-				}
3496
-			}
3497
-		}
3498
-		else {
3499
-			// use normal message or recurring series message
3500
-			$message = $this->message;
3501
-		}
3502
-
3503
-		if (!$message) {
3504
-			return;
3505
-		}
3506
-
3507
-		$newProps = mapi_getprops($message, [$this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter']]);
3508
-
3509
-		// Check whether message is updated or not.
3510
-		if (isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0) {
3511
-			return;
3512
-		}
3513
-
3514
-		if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']]) ||
3515
-			($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']]) ||
3516
-			$isRecurrenceChanged) {
3517
-			$this->clearRecipientResponse($message);
3518
-
3519
-			mapi_setprops($message, [$this->proptags['owner_critical_change'] => time()]);
3520
-
3521
-			mapi_savechanges($message);
3522
-			if ($attach) { // Also save attachment Object.
3523
-				mapi_savechanges($attach);
3524
-			}
3525
-		}
3526
-	}
3527
-
3528
-	/**
3529
-	 * Clear responses of all attendees who have replied in past.
3530
-	 *
3531
-	 * @param MAPI_MESSAGE $message on which responses should be cleared
3532
-	 */
3533
-	public function clearRecipientResponse($message) {
3534
-		$recipTable = mapi_message_getrecipienttable($message);
3535
-		$recipsRows = mapi_table_queryallrows($recipTable, $this->recipprops);
3536
-
3537
-		foreach ($recipsRows as $recipient) {
3538
-			if (($recipient[PR_RECIPIENT_FLAGS] & recipOrganizer) != recipOrganizer) {
3539
-				// Recipient is attendee, set the trackstatus to 'Not Responded'
3540
-				$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3541
-			}
3542
-			else {
3543
-				// Recipient is organizer, this is not possible, but for safety
3544
-				// it is best to clear the trackstatus for him as well by setting
3545
-				// the trackstatus to 'Organized'.
3546
-				$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3547
-			}
3548
-			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, [$recipient]);
3549
-		}
3550
-	}
3551
-
3552
-	/**
3553
-	 * Function returns correspondent calendar item attached with the meeting request/response/cancellation.
3554
-	 * This will only check for actual MAPIMessages in calendar folder, so if a meeting request is
3555
-	 * for exception then this function will return recurring series for that meeting request
3556
-	 * after that you need to use getExceptionItem function to get exception item that will be
3557
-	 * fetched from the attachment table of recurring series MAPIMessage.
3558
-	 *
3559
-	 * @param bool $open boolean to indicate the function should return entryid or MAPIMessage. Defaults to true.
3560
-	 *
3561
-	 * @return entryid or MAPIMessage resource of calendar item
3562
-	 */
3563
-	public function getCorrespondentCalendarItem($open = true) {
3564
-		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
3565
-
3566
-		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
3567
-			// can work only with meeting requests/responses/cancellations
3568
-			return false;
3569
-		}
3570
-
3571
-		$globalId = $props[$this->proptags['goid']];
3572
-		$cleanGlobalId = $props[$this->proptags['goid2']];
3573
-
3574
-		// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3575
-		if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3576
-			$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3577
-
3578
-			$store = $delegatorStore['store'];
3579
-			$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3580
-		}
3581
-		else {
3582
-			$store = $this->store;
3583
-			$calFolder = $this->openDefaultCalendar();
3584
-		}
3585
-
3586
-		$basedate = $this->getBasedateFromGlobalID($globalId);
3587
-
3588
-		/**
3589
-		 * First search for any appointments which correspond to the $globalId,
3590
-		 * this can be the entire series (if the Meeting Request refers to the
3591
-		 * entire series), or an particular Occurrence (if the meeting Request
3592
-		 * contains a basedate).
3593
-		 *
3594
-		 * If we cannot find a corresponding item, and the $globalId contains
3595
-		 * a $basedate, it might imply that a new exception will have to be
3596
-		 * created for a series which is present in the calendar, we can look
3597
-		 * that one up by searching for the $cleanGlobalId.
3598
-		 */
3599
-		$entryids = $this->findCalendarItems($globalId, $calFolder);
3600
-		if ($basedate && empty($entryids)) {
3601
-			$entryids = $this->findCalendarItems($cleanGlobalId, $calFolder, true);
3602
-		}
3603
-
3604
-		// there should be only one item returned
3605
-		if (!empty($entryids) && count($entryids) === 1) {
3606
-			// return only entryid
3607
-			if ($open === false) {
3608
-				return $entryids[0];
3609
-			}
3610
-
3611
-			// open calendar item and return it
3612
-			return mapi_msgstore_openentry($store, $entryids[0]);
3613
-		}
3614
-
3615
-		// no items found in calendar
3616
-		return false;
3617
-	}
3618
-
3619
-	/**
3620
-	 * Function returns exception item based on the basedate passed.
3621
-	 *
3622
-	 * @param MAPIMessage $recurringMessage Resource of Recurring meeting from calendar
3623
-	 * @param Unixtime    $basedate         basedate of exception that needs to be returned
3624
-	 * @param MAPIStore   $store            store that contains the recurring calendar item
3625
-	 *
3626
-	 * @return entryid or MAPIMessage resource of exception item
3627
-	 */
3628
-	public function getExceptionItem($recurringMessage, $basedate, $store = false) {
3629
-		$occurItem = false;
3630
-
3631
-		$props = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID, $this->proptags['recurring']]);
3632
-
3633
-		// check if the passed item is recurring series
3634
-		if ($props[$this->proptags['recurring']] !== false) {
3635
-			return false;
3636
-		}
3637
-
3638
-		if ($store === false) {
3639
-			// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3640
-			if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3641
-				$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID]);
3642
-				$store = $delegatorStore['store'];
3643
-			}
3644
-			else {
3645
-				$store = $this->store;
3646
-			}
3647
-		}
3648
-
3649
-		$recurr = new Recurrence($store, $recurringMessage);
3650
-		$attach = $recurr->getExceptionAttachment($basedate);
3651
-		if ($attach) {
3652
-			$occurItem = mapi_attach_openobj($attach);
3653
-		}
3654
-
3655
-		return $occurItem;
3656
-	}
3657
-
3658
-	/**
3659
-	 * Function which checks whether received meeting request is either conflicting with other appointments or not.
3660
-	 *
3661
-	 * @param MAPIMessage $message   meeting request item that should be checked for conflicts in calendar
3662
-	 * @param MAPIStore   $userStore store containing calendar folder that will be used for confilict checking
3663
-	 * @param MAPIFolder  $calFolder calendar folder for conflict checking
3664
-	 *
3665
-	 * @return mixed(boolean/integer) true if normal meeting is conflicting or an integer which specifies no of instances
3666
-	 * conflict of recurring meeting and false if meeting is not conflicting
3667
-	 * @return mixed if boolean then true/false for indicating conflict, if number then items that are conflicting with the message
3668
-	 */
3669
-	public function isMeetingConflicting($message = false, $userStore = false, $calFolder = false) {
3670
-		$returnValue = false;
3671
-		$noOfInstances = 0;
3672
-
3673
-		if ($message === false) {
3674
-			$message = $this->message;
3675
-		}
3676
-
3677
-		$messageProps = mapi_getprops(
3678
-			$message,
3679
-			[
3680
-				PR_MESSAGE_CLASS,
3681
-				$this->proptags['goid'],
3682
-				$this->proptags['goid2'],
3683
-				$this->proptags['startdate'],
3684
-				$this->proptags['duedate'],
3685
-				$this->proptags['recurring'],
3686
-				$this->proptags['clipstart'],
3687
-				$this->proptags['clipend'],
3688
-				PR_RCVD_REPRESENTING_ENTRYID,
3689
-				$this->proptags['basedate'],
3690
-				PR_RCVD_REPRESENTING_NAME,
3691
-			]
3692
-		);
3693
-
3694
-		if ($userStore === false) {
3695
-			$userStore = $this->store;
3696
-
3697
-			// check if delegate is processing the response
3698
-			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
3699
-				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3700
-
3701
-				$userStore = $delegatorStore['store'];
3702
-				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3703
-			}
3704
-		}
3705
-
3706
-		if ($calFolder === false) {
3707
-			$calFolder = $this->openDefaultCalendar($userStore);
3708
-		}
3709
-
3710
-		if ($calFolder) {
3711
-			// Meeting request is recurring, so get all occurrence and check for each occurrence whether it conflicts with other appointments in Calendar.
3712
-			if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
3713
-				// Apply recurrence class and retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3714
-				$recurr = new Recurrence($userStore, $message);
3715
-				$items = $recurr->getItems($messageProps[$this->proptags['clipstart']], $messageProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3716
-
3717
-				foreach ($items as $item) {
3718
-					// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3719
-					$calendarItems = $recurr->getCalendarItems($userStore, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3720
-
3721
-					foreach ($calendarItems as $calendarItem) {
3722
-						if ($calendarItem[$this->proptags['busystatus']] !== fbFree) {
3723
-							/*
3458
+                if (isset($calendarItemProps[$this->proptags['updatecounter']], $props[$this->proptags['updatecounter']])) {
3459
+                    if ($props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) {
3460
+                        $result = true;
3461
+                    }
3462
+                }
3463
+            }
3464
+        }
3465
+
3466
+        return $result;
3467
+    }
3468
+
3469
+    /**
3470
+     * Checks if there has been any significant changes on appointment/meeting item.
3471
+     * Significant changes be:
3472
+     * 1) startdate has been changed
3473
+     * 2) duedate has been changed OR
3474
+     * 3) recurrence pattern has been created, modified or removed.
3475
+     *
3476
+     * @param array oldProps old props before an update
3477
+     * @param Number basedate basedate
3478
+     * @param bool isRecurrenceChanged for change in recurrence pattern.
3479
+     * isRecurrenceChanged true means Recurrence pattern has been changed, so clear all attendees response
3480
+     * @param mixed $oldProps
3481
+     * @param mixed $basedate
3482
+     * @param mixed $isRecurrenceChanged
3483
+     */
3484
+    public function checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged = false) {
3485
+        $message = null;
3486
+        $attach = null;
3487
+
3488
+        // If basedate is specified then we need to open exception message to clear recipient responses
3489
+        if ($basedate) {
3490
+            $recurrence = new Recurrence($this->store, $this->message);
3491
+            if ($recurrence->isException($basedate)) {
3492
+                $attach = $recurrence->getExceptionAttachment($basedate);
3493
+                if ($attach) {
3494
+                    $message = mapi_attach_openobj($attach, MAPI_MODIFY);
3495
+                }
3496
+            }
3497
+        }
3498
+        else {
3499
+            // use normal message or recurring series message
3500
+            $message = $this->message;
3501
+        }
3502
+
3503
+        if (!$message) {
3504
+            return;
3505
+        }
3506
+
3507
+        $newProps = mapi_getprops($message, [$this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter']]);
3508
+
3509
+        // Check whether message is updated or not.
3510
+        if (isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0) {
3511
+            return;
3512
+        }
3513
+
3514
+        if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']]) ||
3515
+            ($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']]) ||
3516
+            $isRecurrenceChanged) {
3517
+            $this->clearRecipientResponse($message);
3518
+
3519
+            mapi_setprops($message, [$this->proptags['owner_critical_change'] => time()]);
3520
+
3521
+            mapi_savechanges($message);
3522
+            if ($attach) { // Also save attachment Object.
3523
+                mapi_savechanges($attach);
3524
+            }
3525
+        }
3526
+    }
3527
+
3528
+    /**
3529
+     * Clear responses of all attendees who have replied in past.
3530
+     *
3531
+     * @param MAPI_MESSAGE $message on which responses should be cleared
3532
+     */
3533
+    public function clearRecipientResponse($message) {
3534
+        $recipTable = mapi_message_getrecipienttable($message);
3535
+        $recipsRows = mapi_table_queryallrows($recipTable, $this->recipprops);
3536
+
3537
+        foreach ($recipsRows as $recipient) {
3538
+            if (($recipient[PR_RECIPIENT_FLAGS] & recipOrganizer) != recipOrganizer) {
3539
+                // Recipient is attendee, set the trackstatus to 'Not Responded'
3540
+                $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3541
+            }
3542
+            else {
3543
+                // Recipient is organizer, this is not possible, but for safety
3544
+                // it is best to clear the trackstatus for him as well by setting
3545
+                // the trackstatus to 'Organized'.
3546
+                $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3547
+            }
3548
+            mapi_message_modifyrecipients($message, MODRECIP_MODIFY, [$recipient]);
3549
+        }
3550
+    }
3551
+
3552
+    /**
3553
+     * Function returns correspondent calendar item attached with the meeting request/response/cancellation.
3554
+     * This will only check for actual MAPIMessages in calendar folder, so if a meeting request is
3555
+     * for exception then this function will return recurring series for that meeting request
3556
+     * after that you need to use getExceptionItem function to get exception item that will be
3557
+     * fetched from the attachment table of recurring series MAPIMessage.
3558
+     *
3559
+     * @param bool $open boolean to indicate the function should return entryid or MAPIMessage. Defaults to true.
3560
+     *
3561
+     * @return entryid or MAPIMessage resource of calendar item
3562
+     */
3563
+    public function getCorrespondentCalendarItem($open = true) {
3564
+        $props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
3565
+
3566
+        if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
3567
+            // can work only with meeting requests/responses/cancellations
3568
+            return false;
3569
+        }
3570
+
3571
+        $globalId = $props[$this->proptags['goid']];
3572
+        $cleanGlobalId = $props[$this->proptags['goid2']];
3573
+
3574
+        // If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3575
+        if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3576
+            $delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3577
+
3578
+            $store = $delegatorStore['store'];
3579
+            $calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3580
+        }
3581
+        else {
3582
+            $store = $this->store;
3583
+            $calFolder = $this->openDefaultCalendar();
3584
+        }
3585
+
3586
+        $basedate = $this->getBasedateFromGlobalID($globalId);
3587
+
3588
+        /**
3589
+         * First search for any appointments which correspond to the $globalId,
3590
+         * this can be the entire series (if the Meeting Request refers to the
3591
+         * entire series), or an particular Occurrence (if the meeting Request
3592
+         * contains a basedate).
3593
+         *
3594
+         * If we cannot find a corresponding item, and the $globalId contains
3595
+         * a $basedate, it might imply that a new exception will have to be
3596
+         * created for a series which is present in the calendar, we can look
3597
+         * that one up by searching for the $cleanGlobalId.
3598
+         */
3599
+        $entryids = $this->findCalendarItems($globalId, $calFolder);
3600
+        if ($basedate && empty($entryids)) {
3601
+            $entryids = $this->findCalendarItems($cleanGlobalId, $calFolder, true);
3602
+        }
3603
+
3604
+        // there should be only one item returned
3605
+        if (!empty($entryids) && count($entryids) === 1) {
3606
+            // return only entryid
3607
+            if ($open === false) {
3608
+                return $entryids[0];
3609
+            }
3610
+
3611
+            // open calendar item and return it
3612
+            return mapi_msgstore_openentry($store, $entryids[0]);
3613
+        }
3614
+
3615
+        // no items found in calendar
3616
+        return false;
3617
+    }
3618
+
3619
+    /**
3620
+     * Function returns exception item based on the basedate passed.
3621
+     *
3622
+     * @param MAPIMessage $recurringMessage Resource of Recurring meeting from calendar
3623
+     * @param Unixtime    $basedate         basedate of exception that needs to be returned
3624
+     * @param MAPIStore   $store            store that contains the recurring calendar item
3625
+     *
3626
+     * @return entryid or MAPIMessage resource of exception item
3627
+     */
3628
+    public function getExceptionItem($recurringMessage, $basedate, $store = false) {
3629
+        $occurItem = false;
3630
+
3631
+        $props = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID, $this->proptags['recurring']]);
3632
+
3633
+        // check if the passed item is recurring series
3634
+        if ($props[$this->proptags['recurring']] !== false) {
3635
+            return false;
3636
+        }
3637
+
3638
+        if ($store === false) {
3639
+            // If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3640
+            if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3641
+                $delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID]);
3642
+                $store = $delegatorStore['store'];
3643
+            }
3644
+            else {
3645
+                $store = $this->store;
3646
+            }
3647
+        }
3648
+
3649
+        $recurr = new Recurrence($store, $recurringMessage);
3650
+        $attach = $recurr->getExceptionAttachment($basedate);
3651
+        if ($attach) {
3652
+            $occurItem = mapi_attach_openobj($attach);
3653
+        }
3654
+
3655
+        return $occurItem;
3656
+    }
3657
+
3658
+    /**
3659
+     * Function which checks whether received meeting request is either conflicting with other appointments or not.
3660
+     *
3661
+     * @param MAPIMessage $message   meeting request item that should be checked for conflicts in calendar
3662
+     * @param MAPIStore   $userStore store containing calendar folder that will be used for confilict checking
3663
+     * @param MAPIFolder  $calFolder calendar folder for conflict checking
3664
+     *
3665
+     * @return mixed(boolean/integer) true if normal meeting is conflicting or an integer which specifies no of instances
3666
+     * conflict of recurring meeting and false if meeting is not conflicting
3667
+     * @return mixed if boolean then true/false for indicating conflict, if number then items that are conflicting with the message
3668
+     */
3669
+    public function isMeetingConflicting($message = false, $userStore = false, $calFolder = false) {
3670
+        $returnValue = false;
3671
+        $noOfInstances = 0;
3672
+
3673
+        if ($message === false) {
3674
+            $message = $this->message;
3675
+        }
3676
+
3677
+        $messageProps = mapi_getprops(
3678
+            $message,
3679
+            [
3680
+                PR_MESSAGE_CLASS,
3681
+                $this->proptags['goid'],
3682
+                $this->proptags['goid2'],
3683
+                $this->proptags['startdate'],
3684
+                $this->proptags['duedate'],
3685
+                $this->proptags['recurring'],
3686
+                $this->proptags['clipstart'],
3687
+                $this->proptags['clipend'],
3688
+                PR_RCVD_REPRESENTING_ENTRYID,
3689
+                $this->proptags['basedate'],
3690
+                PR_RCVD_REPRESENTING_NAME,
3691
+            ]
3692
+        );
3693
+
3694
+        if ($userStore === false) {
3695
+            $userStore = $this->store;
3696
+
3697
+            // check if delegate is processing the response
3698
+            if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
3699
+                $delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3700
+
3701
+                $userStore = $delegatorStore['store'];
3702
+                $calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3703
+            }
3704
+        }
3705
+
3706
+        if ($calFolder === false) {
3707
+            $calFolder = $this->openDefaultCalendar($userStore);
3708
+        }
3709
+
3710
+        if ($calFolder) {
3711
+            // Meeting request is recurring, so get all occurrence and check for each occurrence whether it conflicts with other appointments in Calendar.
3712
+            if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
3713
+                // Apply recurrence class and retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3714
+                $recurr = new Recurrence($userStore, $message);
3715
+                $items = $recurr->getItems($messageProps[$this->proptags['clipstart']], $messageProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3716
+
3717
+                foreach ($items as $item) {
3718
+                    // Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3719
+                    $calendarItems = $recurr->getCalendarItems($userStore, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3720
+
3721
+                    foreach ($calendarItems as $calendarItem) {
3722
+                        if ($calendarItem[$this->proptags['busystatus']] !== fbFree) {
3723
+                            /*
3724 3724
 							 * Only meeting requests have globalID, normal appointments do not have globalID
3725 3725
 							 * so if any normal appointment if found then it is assumed to be conflict.
3726 3726
 							 */
3727
-							if (isset($calendarItem[$this->proptags['goid']])) {
3728
-								if ($calendarItem[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) {
3729
-									++$noOfInstances;
3730
-
3731
-									break;
3732
-								}
3733
-							}
3734
-							else {
3735
-								++$noOfInstances;
3736
-
3737
-								break;
3738
-							}
3739
-						}
3740
-					}
3741
-				}
3742
-
3743
-				if ($noOfInstances > 0) {
3744
-					$returnValue = $noOfInstances;
3745
-				}
3746
-			}
3747
-			else {
3748
-				// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3749
-				$items = getCalendarItems($userStore, $calFolder, $messageProps[$this->proptags['startdate']], $messageProps[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3750
-
3751
-				if (isset($messageProps[$this->proptags['basedate']]) && !empty($messageProps[$this->proptags['basedate']])) {
3752
-					$basedate = $messageProps[$this->proptags['basedate']];
3753
-					// Get the goid2 from recurring MR which further used to
3754
-					// check the resource conflicts item.
3755
-					$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid2']]);
3756
-					$messageProps[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid2']], $basedate);
3757
-					$messageProps[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
3758
-				}
3759
-
3760
-				foreach ($items as $item) {
3761
-					if ($item[$this->proptags['busystatus']] !== fbFree) {
3762
-						if (isset($item[$this->proptags['goid']])) {
3763
-							if (($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) &&
3764
-								($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid2']])) {
3765
-								$returnValue = true;
3766
-
3767
-								break;
3768
-							}
3769
-						}
3770
-						else {
3771
-							$returnValue = true;
3772
-
3773
-							break;
3774
-						}
3775
-					}
3776
-				}
3777
-			}
3778
-		}
3779
-
3780
-		return $returnValue;
3781
-	}
3782
-
3783
-	/**
3784
-	 *  Function which adds organizer to recipient list which is passed.
3785
-	 *  This function also checks if it has organizer.
3786
-	 *
3787
-	 * @param array $messageProps message properties
3788
-	 * @param array $recipients   recipients list of message
3789
-	 * @param bool  $isException  true if we are processing recipient of exception
3790
-	 */
3791
-	public function addDelegator($messageProps, &$recipients) {
3792
-		$hasDelegator = false;
3793
-		// Check if meeting already has an organizer.
3794
-		foreach ($recipients as $key => $recipient) {
3795
-			if (isset($messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) && $recipient[PR_EMAIL_ADDRESS] == $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) {
3796
-				$hasDelegator = true;
3797
-			}
3798
-		}
3799
-
3800
-		if (!$hasDelegator) {
3801
-			// Create delegator.
3802
-			$delegator = [];
3803
-			$delegator[PR_ENTRYID] = $messageProps[PR_RCVD_REPRESENTING_ENTRYID];
3804
-			$delegator[PR_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3805
-			$delegator[PR_EMAIL_ADDRESS] = $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS];
3806
-			$delegator[PR_RECIPIENT_TYPE] = MAPI_TO;
3807
-			$delegator[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3808
-			$delegator[PR_ADDRTYPE] = empty($messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_RCVD_REPRESENTING_ADDRTYPE];
3809
-			$delegator[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3810
-			$delegator[PR_RECIPIENT_FLAGS] = recipSendable;
3811
-			$delegator[PR_SEARCH_KEY] = $messageProps[PR_RCVD_REPRESENTING_SEARCH_KEY];
3812
-
3813
-			// Add organizer to recipients list.
3814
-			array_unshift($recipients, $delegator);
3815
-		}
3816
-	}
3817
-
3818
-	/**
3819
-	 * Function will return delegator's store and calendar folder for processing meetings.
3820
-	 *
3821
-	 * @param string $receivedRepresentingEnryid  entryid of the delegator user
3822
-	 * @param array  $foldersToOpen               contains list of folder types that should be returned in result
3823
-	 * @param mixed  $receivedRepresentingEntryId
3824
-	 *
3825
-	 * @return array contains store of the delegator and resource of folders if $foldersToOpen is not empty
3826
-	 */
3827
-	public function getDelegatorStore($receivedRepresentingEntryId, $foldersToOpen = []) {
3828
-		$returnData = [];
3829
-
3830
-		$delegatorStore = $this->openCustomUserStore($receivedRepresentingEntryId);
3831
-		$returnData['store'] = $delegatorStore;
3832
-
3833
-		if (!empty($foldersToOpen)) {
3834
-			for ($index = 0, $len = count($foldersToOpen); $index < $len; ++$index) {
3835
-				$folderType = $foldersToOpen[$index];
3836
-
3837
-				// first try with default folders
3838
-				$folder = $this->openDefaultFolder($folderType, $delegatorStore);
3839
-
3840
-				// if folder not found then try with base folders
3841
-				if ($folder === false) {
3842
-					$folder = $this->openBaseFolder($folderType, $delegatorStore);
3843
-				}
3844
-
3845
-				if ($folder === false) {
3846
-					// we are still not able to get the folder so give up
3847
-					continue;
3848
-				}
3849
-
3850
-				$returnData[$folderType] = $folder;
3851
-			}
3852
-		}
3853
-
3854
-		return $returnData;
3855
-	}
3856
-
3857
-	/**
3858
-	 * Function returns extra info about meeting timing along with message body
3859
-	 * which will be included in body while sending meeting request/response.
3860
-	 *
3861
-	 * @return string $meetingTimeInfo info about meeting timing along with message body
3862
-	 */
3863
-	public function getMeetingTimeInfo() {
3864
-		return $this->meetingTimeInfo;
3865
-	}
3866
-
3867
-	/**
3868
-	 * Function sets extra info about meeting timing along with message body
3869
-	 * which will be included in body while sending meeting request/response.
3870
-	 *
3871
-	 * @param string $meetingTimeInfo info about meeting timing along with message body
3872
-	 */
3873
-	public function setMeetingTimeInfo($meetingTimeInfo) {
3874
-		$this->meetingTimeInfo = $meetingTimeInfo;
3875
-	}
3876
-
3877
-	/**
3878
-	 * Helper function which is use to get local categories of all occurrence.
3879
-	 *
3880
-	 * @param MAPIMessage $calendarItem meeting request item
3881
-	 * @param MAPIStore   $store        store containing calendar folder
3882
-	 * @param MAPIFolder  $calFolder    calendar folder
3883
-	 *
3884
-	 * @return array $localCategories which contain array of basedate along with categories
3885
-	 */
3886
-	public function getLocalCategories($calendarItem, $store, $calFolder) {
3887
-		$calendarItemProps = mapi_getprops($calendarItem);
3888
-		$recurrence = new Recurrence($store, $calendarItem);
3889
-
3890
-		// Retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3891
-		$items = $recurrence->getItems($calendarItemProps[$this->proptags['clipstart']], $calendarItemProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3892
-		$localCategories = [];
3893
-
3894
-		foreach ($items as $item) {
3895
-			$recurrenceItems = $recurrence->getCalendarItems($store, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus'], $this->proptags['categories']]);
3896
-			foreach ($recurrenceItems as $recurrenceItem) {
3897
-				// Check if occurrence is exception then get the local categories of that occurrence.
3898
-				if (isset($recurrenceItem[$this->proptags['goid']]) && $recurrenceItem[$this->proptags['goid']] == $calendarItemProps[$this->proptags['goid']]) {
3899
-					$exceptionAttach = $recurrence->getExceptionAttachment($recurrenceItem['basedate']);
3900
-
3901
-					if ($exceptionAttach) {
3902
-						$exception = mapi_attach_openobj($exceptionAttach, 0);
3903
-						$exceptionProps = mapi_getprops($exception, [$this->proptags['categories']]);
3904
-						if (isset($exceptionProps[$this->proptags['categories']])) {
3905
-							$localCategories[$recurrenceItem['basedate']] = $exceptionProps[$this->proptags['categories']];
3906
-						}
3907
-					}
3908
-				}
3909
-			}
3910
-		}
3911
-
3912
-		return $localCategories;
3913
-	}
3914
-
3915
-	/**
3916
-	 * Helper function which is use to apply local categories on respective occurrences.
3917
-	 *
3918
-	 * @param MAPIMessage $calendarItem    meeting request item
3919
-	 * @param MAPIStore   $store           store containing calendar folder
3920
-	 * @param array       $localCategories array contains basedate and array of categories
3921
-	 */
3922
-	public function applyLocalCategories($calendarItem, $store, $localCategories) {
3923
-		$calendarItemProps = mapi_getprops($calendarItem, [PR_PARENT_ENTRYID, PR_ENTRYID]);
3924
-		$message = mapi_msgstore_openentry($store, $calendarItemProps[PR_ENTRYID]);
3925
-		$recurrence = new Recurrence($store, $message);
3926
-
3927
-		// Check for all occurrence if it is exception then modify the exception by setting up categories,
3928
-		// Otherwise create new exception with categories.
3929
-		foreach ($localCategories as $key => $value) {
3930
-			if ($recurrence->isException($key)) {
3931
-				$recurrence->modifyException([$this->proptags['categories'] => $value], $key);
3932
-			}
3933
-			else {
3934
-				$recurrence->createException([$this->proptags['categories'] => $value], $key, false);
3935
-			}
3936
-			mapi_savechanges($message);
3937
-		}
3938
-	}
3727
+                            if (isset($calendarItem[$this->proptags['goid']])) {
3728
+                                if ($calendarItem[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) {
3729
+                                    ++$noOfInstances;
3730
+
3731
+                                    break;
3732
+                                }
3733
+                            }
3734
+                            else {
3735
+                                ++$noOfInstances;
3736
+
3737
+                                break;
3738
+                            }
3739
+                        }
3740
+                    }
3741
+                }
3742
+
3743
+                if ($noOfInstances > 0) {
3744
+                    $returnValue = $noOfInstances;
3745
+                }
3746
+            }
3747
+            else {
3748
+                // Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3749
+                $items = getCalendarItems($userStore, $calFolder, $messageProps[$this->proptags['startdate']], $messageProps[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3750
+
3751
+                if (isset($messageProps[$this->proptags['basedate']]) && !empty($messageProps[$this->proptags['basedate']])) {
3752
+                    $basedate = $messageProps[$this->proptags['basedate']];
3753
+                    // Get the goid2 from recurring MR which further used to
3754
+                    // check the resource conflicts item.
3755
+                    $recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid2']]);
3756
+                    $messageProps[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid2']], $basedate);
3757
+                    $messageProps[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
3758
+                }
3759
+
3760
+                foreach ($items as $item) {
3761
+                    if ($item[$this->proptags['busystatus']] !== fbFree) {
3762
+                        if (isset($item[$this->proptags['goid']])) {
3763
+                            if (($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) &&
3764
+                                ($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid2']])) {
3765
+                                $returnValue = true;
3766
+
3767
+                                break;
3768
+                            }
3769
+                        }
3770
+                        else {
3771
+                            $returnValue = true;
3772
+
3773
+                            break;
3774
+                        }
3775
+                    }
3776
+                }
3777
+            }
3778
+        }
3779
+
3780
+        return $returnValue;
3781
+    }
3782
+
3783
+    /**
3784
+     *  Function which adds organizer to recipient list which is passed.
3785
+     *  This function also checks if it has organizer.
3786
+     *
3787
+     * @param array $messageProps message properties
3788
+     * @param array $recipients   recipients list of message
3789
+     * @param bool  $isException  true if we are processing recipient of exception
3790
+     */
3791
+    public function addDelegator($messageProps, &$recipients) {
3792
+        $hasDelegator = false;
3793
+        // Check if meeting already has an organizer.
3794
+        foreach ($recipients as $key => $recipient) {
3795
+            if (isset($messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) && $recipient[PR_EMAIL_ADDRESS] == $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) {
3796
+                $hasDelegator = true;
3797
+            }
3798
+        }
3799
+
3800
+        if (!$hasDelegator) {
3801
+            // Create delegator.
3802
+            $delegator = [];
3803
+            $delegator[PR_ENTRYID] = $messageProps[PR_RCVD_REPRESENTING_ENTRYID];
3804
+            $delegator[PR_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3805
+            $delegator[PR_EMAIL_ADDRESS] = $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS];
3806
+            $delegator[PR_RECIPIENT_TYPE] = MAPI_TO;
3807
+            $delegator[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3808
+            $delegator[PR_ADDRTYPE] = empty($messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_RCVD_REPRESENTING_ADDRTYPE];
3809
+            $delegator[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3810
+            $delegator[PR_RECIPIENT_FLAGS] = recipSendable;
3811
+            $delegator[PR_SEARCH_KEY] = $messageProps[PR_RCVD_REPRESENTING_SEARCH_KEY];
3812
+
3813
+            // Add organizer to recipients list.
3814
+            array_unshift($recipients, $delegator);
3815
+        }
3816
+    }
3817
+
3818
+    /**
3819
+     * Function will return delegator's store and calendar folder for processing meetings.
3820
+     *
3821
+     * @param string $receivedRepresentingEnryid  entryid of the delegator user
3822
+     * @param array  $foldersToOpen               contains list of folder types that should be returned in result
3823
+     * @param mixed  $receivedRepresentingEntryId
3824
+     *
3825
+     * @return array contains store of the delegator and resource of folders if $foldersToOpen is not empty
3826
+     */
3827
+    public function getDelegatorStore($receivedRepresentingEntryId, $foldersToOpen = []) {
3828
+        $returnData = [];
3829
+
3830
+        $delegatorStore = $this->openCustomUserStore($receivedRepresentingEntryId);
3831
+        $returnData['store'] = $delegatorStore;
3832
+
3833
+        if (!empty($foldersToOpen)) {
3834
+            for ($index = 0, $len = count($foldersToOpen); $index < $len; ++$index) {
3835
+                $folderType = $foldersToOpen[$index];
3836
+
3837
+                // first try with default folders
3838
+                $folder = $this->openDefaultFolder($folderType, $delegatorStore);
3839
+
3840
+                // if folder not found then try with base folders
3841
+                if ($folder === false) {
3842
+                    $folder = $this->openBaseFolder($folderType, $delegatorStore);
3843
+                }
3844
+
3845
+                if ($folder === false) {
3846
+                    // we are still not able to get the folder so give up
3847
+                    continue;
3848
+                }
3849
+
3850
+                $returnData[$folderType] = $folder;
3851
+            }
3852
+        }
3853
+
3854
+        return $returnData;
3855
+    }
3856
+
3857
+    /**
3858
+     * Function returns extra info about meeting timing along with message body
3859
+     * which will be included in body while sending meeting request/response.
3860
+     *
3861
+     * @return string $meetingTimeInfo info about meeting timing along with message body
3862
+     */
3863
+    public function getMeetingTimeInfo() {
3864
+        return $this->meetingTimeInfo;
3865
+    }
3866
+
3867
+    /**
3868
+     * Function sets extra info about meeting timing along with message body
3869
+     * which will be included in body while sending meeting request/response.
3870
+     *
3871
+     * @param string $meetingTimeInfo info about meeting timing along with message body
3872
+     */
3873
+    public function setMeetingTimeInfo($meetingTimeInfo) {
3874
+        $this->meetingTimeInfo = $meetingTimeInfo;
3875
+    }
3876
+
3877
+    /**
3878
+     * Helper function which is use to get local categories of all occurrence.
3879
+     *
3880
+     * @param MAPIMessage $calendarItem meeting request item
3881
+     * @param MAPIStore   $store        store containing calendar folder
3882
+     * @param MAPIFolder  $calFolder    calendar folder
3883
+     *
3884
+     * @return array $localCategories which contain array of basedate along with categories
3885
+     */
3886
+    public function getLocalCategories($calendarItem, $store, $calFolder) {
3887
+        $calendarItemProps = mapi_getprops($calendarItem);
3888
+        $recurrence = new Recurrence($store, $calendarItem);
3889
+
3890
+        // Retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3891
+        $items = $recurrence->getItems($calendarItemProps[$this->proptags['clipstart']], $calendarItemProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3892
+        $localCategories = [];
3893
+
3894
+        foreach ($items as $item) {
3895
+            $recurrenceItems = $recurrence->getCalendarItems($store, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus'], $this->proptags['categories']]);
3896
+            foreach ($recurrenceItems as $recurrenceItem) {
3897
+                // Check if occurrence is exception then get the local categories of that occurrence.
3898
+                if (isset($recurrenceItem[$this->proptags['goid']]) && $recurrenceItem[$this->proptags['goid']] == $calendarItemProps[$this->proptags['goid']]) {
3899
+                    $exceptionAttach = $recurrence->getExceptionAttachment($recurrenceItem['basedate']);
3900
+
3901
+                    if ($exceptionAttach) {
3902
+                        $exception = mapi_attach_openobj($exceptionAttach, 0);
3903
+                        $exceptionProps = mapi_getprops($exception, [$this->proptags['categories']]);
3904
+                        if (isset($exceptionProps[$this->proptags['categories']])) {
3905
+                            $localCategories[$recurrenceItem['basedate']] = $exceptionProps[$this->proptags['categories']];
3906
+                        }
3907
+                    }
3908
+                }
3909
+            }
3910
+        }
3911
+
3912
+        return $localCategories;
3913
+    }
3914
+
3915
+    /**
3916
+     * Helper function which is use to apply local categories on respective occurrences.
3917
+     *
3918
+     * @param MAPIMessage $calendarItem    meeting request item
3919
+     * @param MAPIStore   $store           store containing calendar folder
3920
+     * @param array       $localCategories array contains basedate and array of categories
3921
+     */
3922
+    public function applyLocalCategories($calendarItem, $store, $localCategories) {
3923
+        $calendarItemProps = mapi_getprops($calendarItem, [PR_PARENT_ENTRYID, PR_ENTRYID]);
3924
+        $message = mapi_msgstore_openentry($store, $calendarItemProps[PR_ENTRYID]);
3925
+        $recurrence = new Recurrence($store, $message);
3926
+
3927
+        // Check for all occurrence if it is exception then modify the exception by setting up categories,
3928
+        // Otherwise create new exception with categories.
3929
+        foreach ($localCategories as $key => $value) {
3930
+            if ($recurrence->isException($key)) {
3931
+                $recurrence->modifyException([$this->proptags['categories'] => $value], $key);
3932
+            }
3933
+            else {
3934
+                $recurrence->createException([$this->proptags['categories'] => $value], $key, false);
3935
+            }
3936
+            mapi_savechanges($message);
3937
+        }
3938
+    }
3939 3939
 }
Please login to merge, or discard this patch.
mapi/class.taskrecurrence.php 1 patch
Indentation   +405 added lines, -405 removed lines patch added patch discarded remove patch
@@ -5,411 +5,411 @@
 block discarded – undo
5 5
  * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
6 6
  */
7 7
 
8
-	class TaskRecurrence extends BaseRecurrence {
9
-		/**
10
-		 * Timezone info which is always false for task.
11
-		 */
12
-		public $tz = false;
13
-
14
-		private $action;
15
-
16
-		public function __construct($store, $message) {
17
-			$this->store = $store;
18
-			$this->message = $message;
19
-
20
-			$properties = [];
21
-			$properties["entryid"] = PR_ENTRYID;
22
-			$properties["parent_entryid"] = PR_PARENT_ENTRYID;
23
-			$properties["icon_index"] = PR_ICON_INDEX;
24
-			$properties["message_class"] = PR_MESSAGE_CLASS;
25
-			$properties["message_flags"] = PR_MESSAGE_FLAGS;
26
-			$properties["subject"] = PR_SUBJECT;
27
-			$properties["importance"] = PR_IMPORTANCE;
28
-			$properties["sensitivity"] = PR_SENSITIVITY;
29
-			$properties["last_modification_time"] = PR_LAST_MODIFICATION_TIME;
30
-			$properties["status"] = "PT_LONG:PSETID_Task:0x8101";
31
-			$properties["percent_complete"] = "PT_DOUBLE:PSETID_Task:0x8102";
32
-			$properties["startdate"] = "PT_SYSTIME:PSETID_Task:0x8104";
33
-			$properties["duedate"] = "PT_SYSTIME:PSETID_Task:0x8105";
34
-			$properties["reset_reminder"] = "PT_BOOLEAN:PSETID_Task:0x8107";
35
-			$properties["dead_occurrence"] = "PT_BOOLEAN:PSETID_Task:0x8109";
36
-			$properties["datecompleted"] = "PT_SYSTIME:PSETID_Task:0x810f";
37
-			$properties["recurring_data"] = "PT_BINARY:PSETID_Task:0x8116";
38
-			$properties["actualwork"] = "PT_LONG:PSETID_Task:0x8110";
39
-			$properties["totalwork"] = "PT_LONG:PSETID_Task:0x8111";
40
-			$properties["complete"] = "PT_BOOLEAN:PSETID_Task:0x811c";
41
-			$properties["task_f_creator"] = "PT_BOOLEAN:PSETID_Task:0x811e";
42
-			$properties["owner"] = "PT_STRING8:PSETID_Task:0x811f";
43
-			$properties["recurring"] = "PT_BOOLEAN:PSETID_Task:0x8126";
44
-
45
-			$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
46
-			$properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502";
47
-			$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
48
-
49
-			$properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
50
-			$properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
51
-			$properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
52
-			$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
53
-
54
-			$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
55
-			$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
56
-			$properties["commonassign"] = "PT_LONG:PSETID_Common:0x8518";
57
-			$properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:0x8560";
58
-			$properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
59
-			$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
60
-			$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
61
-
62
-			$this->proptags = getPropIdsFromStrings($store, $properties);
63
-
64
-			parent::__construct($store, $message, $properties);
65
-		}
66
-
67
-		/**
68
-		 * Function which saves recurrence and also regenerates task if necessary.
69
-		 *
70
-		 *@param array $recur new recurrence properties
71
-		 *
72
-		 *@return array of properties of regenerated task else false
73
-		 */
74
-		public function setRecurrence(&$recur) {
75
-			$this->recur = $recur;
76
-			$this->action = &$recur;
77
-
78
-			if (!isset($this->recur["changed_occurrences"])) {
79
-				$this->recur["changed_occurrences"] = [];
80
-			}
81
-
82
-			if (!isset($this->recur["deleted_occurrences"])) {
83
-				$this->recur["deleted_occurrences"] = [];
84
-			}
85
-
86
-			if (!isset($this->recur['startocc'])) {
87
-				$this->recur['startocc'] = 0;
88
-			}
89
-			if (!isset($this->recur['endocc'])) {
90
-				$this->recur['endocc'] = 0;
91
-			}
92
-
93
-			// Save recurrence because we need proper startrecurrdate and endrecurrdate
94
-			$this->saveRecurrence();
95
-
96
-			// Update $this->recur with proper startrecurrdate and endrecurrdate updated after saving recurrence
97
-			$msgProps = mapi_getprops($this->message, [$this->proptags['recurring_data']]);
98
-			$recurring_data = $this->parseRecurrence($msgProps[$this->proptags['recurring_data']]);
99
-			foreach ($recurring_data as $key => $value) {
100
-				$this->recur[$key] = $value;
101
-			}
102
-
103
-			$this->setFirstOccurrence();
104
-
105
-			// Let's see if next occurrence has to be generated
106
-			return $this->moveToNextOccurrence();
107
-		}
108
-
109
-		/**
110
-		 * Sets task object to first occurrence if startdate/duedate of task object is different from first occurrence.
111
-		 */
112
-		public function setFirstOccurrence() {
113
-			// Check if it is already the first occurrence
114
-			if ($this->action['start'] == $this->recur["start"]) {
115
-				return;
116
-			}
117
-			$items = $this->getNextOccurrence();
118
-
119
-			$props = [];
120
-			$props[$this->proptags['startdate']] = $items[$this->proptags['startdate']];
121
-			$props[$this->proptags['commonstart']] = $items[$this->proptags['startdate']];
122
-
123
-			$props[$this->proptags['duedate']] = $items[$this->proptags['duedate']];
124
-			$props[$this->proptags['commonend']] = $items[$this->proptags['duedate']];
125
-
126
-			mapi_setprops($this->message, $props);
127
-		}
128
-
129
-		/**
130
-		 * Function which creates new task as current occurrence and moves the
131
-		 * existing task to next occurrence.
132
-		 *
133
-		 *@param array $recur $action from client
134
-		 *
135
-		 *@return bool if moving to next occurrence succeed then it returns
136
-		 *        properties of either newly created task or existing task ELSE
137
-		 *        false because that was last occurrence
138
-		 */
139
-		public function moveToNextOccurrence() {
140
-			$result = false;
141
-			/*
8
+    class TaskRecurrence extends BaseRecurrence {
9
+        /**
10
+         * Timezone info which is always false for task.
11
+         */
12
+        public $tz = false;
13
+
14
+        private $action;
15
+
16
+        public function __construct($store, $message) {
17
+            $this->store = $store;
18
+            $this->message = $message;
19
+
20
+            $properties = [];
21
+            $properties["entryid"] = PR_ENTRYID;
22
+            $properties["parent_entryid"] = PR_PARENT_ENTRYID;
23
+            $properties["icon_index"] = PR_ICON_INDEX;
24
+            $properties["message_class"] = PR_MESSAGE_CLASS;
25
+            $properties["message_flags"] = PR_MESSAGE_FLAGS;
26
+            $properties["subject"] = PR_SUBJECT;
27
+            $properties["importance"] = PR_IMPORTANCE;
28
+            $properties["sensitivity"] = PR_SENSITIVITY;
29
+            $properties["last_modification_time"] = PR_LAST_MODIFICATION_TIME;
30
+            $properties["status"] = "PT_LONG:PSETID_Task:0x8101";
31
+            $properties["percent_complete"] = "PT_DOUBLE:PSETID_Task:0x8102";
32
+            $properties["startdate"] = "PT_SYSTIME:PSETID_Task:0x8104";
33
+            $properties["duedate"] = "PT_SYSTIME:PSETID_Task:0x8105";
34
+            $properties["reset_reminder"] = "PT_BOOLEAN:PSETID_Task:0x8107";
35
+            $properties["dead_occurrence"] = "PT_BOOLEAN:PSETID_Task:0x8109";
36
+            $properties["datecompleted"] = "PT_SYSTIME:PSETID_Task:0x810f";
37
+            $properties["recurring_data"] = "PT_BINARY:PSETID_Task:0x8116";
38
+            $properties["actualwork"] = "PT_LONG:PSETID_Task:0x8110";
39
+            $properties["totalwork"] = "PT_LONG:PSETID_Task:0x8111";
40
+            $properties["complete"] = "PT_BOOLEAN:PSETID_Task:0x811c";
41
+            $properties["task_f_creator"] = "PT_BOOLEAN:PSETID_Task:0x811e";
42
+            $properties["owner"] = "PT_STRING8:PSETID_Task:0x811f";
43
+            $properties["recurring"] = "PT_BOOLEAN:PSETID_Task:0x8126";
44
+
45
+            $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
46
+            $properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502";
47
+            $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
48
+
49
+            $properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
50
+            $properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
51
+            $properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
52
+            $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
53
+
54
+            $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
55
+            $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
56
+            $properties["commonassign"] = "PT_LONG:PSETID_Common:0x8518";
57
+            $properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:0x8560";
58
+            $properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
59
+            $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
60
+            $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
61
+
62
+            $this->proptags = getPropIdsFromStrings($store, $properties);
63
+
64
+            parent::__construct($store, $message, $properties);
65
+        }
66
+
67
+        /**
68
+         * Function which saves recurrence and also regenerates task if necessary.
69
+         *
70
+         *@param array $recur new recurrence properties
71
+         *
72
+         *@return array of properties of regenerated task else false
73
+         */
74
+        public function setRecurrence(&$recur) {
75
+            $this->recur = $recur;
76
+            $this->action = &$recur;
77
+
78
+            if (!isset($this->recur["changed_occurrences"])) {
79
+                $this->recur["changed_occurrences"] = [];
80
+            }
81
+
82
+            if (!isset($this->recur["deleted_occurrences"])) {
83
+                $this->recur["deleted_occurrences"] = [];
84
+            }
85
+
86
+            if (!isset($this->recur['startocc'])) {
87
+                $this->recur['startocc'] = 0;
88
+            }
89
+            if (!isset($this->recur['endocc'])) {
90
+                $this->recur['endocc'] = 0;
91
+            }
92
+
93
+            // Save recurrence because we need proper startrecurrdate and endrecurrdate
94
+            $this->saveRecurrence();
95
+
96
+            // Update $this->recur with proper startrecurrdate and endrecurrdate updated after saving recurrence
97
+            $msgProps = mapi_getprops($this->message, [$this->proptags['recurring_data']]);
98
+            $recurring_data = $this->parseRecurrence($msgProps[$this->proptags['recurring_data']]);
99
+            foreach ($recurring_data as $key => $value) {
100
+                $this->recur[$key] = $value;
101
+            }
102
+
103
+            $this->setFirstOccurrence();
104
+
105
+            // Let's see if next occurrence has to be generated
106
+            return $this->moveToNextOccurrence();
107
+        }
108
+
109
+        /**
110
+         * Sets task object to first occurrence if startdate/duedate of task object is different from first occurrence.
111
+         */
112
+        public function setFirstOccurrence() {
113
+            // Check if it is already the first occurrence
114
+            if ($this->action['start'] == $this->recur["start"]) {
115
+                return;
116
+            }
117
+            $items = $this->getNextOccurrence();
118
+
119
+            $props = [];
120
+            $props[$this->proptags['startdate']] = $items[$this->proptags['startdate']];
121
+            $props[$this->proptags['commonstart']] = $items[$this->proptags['startdate']];
122
+
123
+            $props[$this->proptags['duedate']] = $items[$this->proptags['duedate']];
124
+            $props[$this->proptags['commonend']] = $items[$this->proptags['duedate']];
125
+
126
+            mapi_setprops($this->message, $props);
127
+        }
128
+
129
+        /**
130
+         * Function which creates new task as current occurrence and moves the
131
+         * existing task to next occurrence.
132
+         *
133
+         *@param array $recur $action from client
134
+         *
135
+         *@return bool if moving to next occurrence succeed then it returns
136
+         *        properties of either newly created task or existing task ELSE
137
+         *        false because that was last occurrence
138
+         */
139
+        public function moveToNextOccurrence() {
140
+            $result = false;
141
+            /*
142 142
 			 * Every recurring task should have a 'duedate'. If a recurring task is created with no start/end date
143 143
 			 * then we create first two occurrence separately and for first occurrence recurrence has ended.
144 144
 			 */
145
-			if ((empty($this->action['startdate']) && empty($this->action['duedate'])) ||
146
-				($this->action['complete'] == 1) || (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence'])) {
147
-				$nextOccurrence = $this->getNextOccurrence();
148
-				$result = mapi_getprops($this->message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
149
-
150
-				$props = [];
151
-				if ($nextOccurrence) {
152
-					if (!isset($this->action['deleteOccurrence'])) {
153
-						// Create current occurrence as separate task
154
-						$result = $this->regenerateTask($this->action['complete']);
155
-					}
156
-
157
-					// Set reminder for next occurrence
158
-					$this->setReminder($nextOccurrence);
159
-
160
-					// Update properties for next occurrence
161
-					$this->action['duedate'] = $props[$this->proptags['duedate']] = $nextOccurrence[$this->proptags['duedate']];
162
-					$this->action['commonend'] = $props[$this->proptags['commonend']] = $nextOccurrence[$this->proptags['duedate']];
163
-
164
-					$this->action['startdate'] = $props[$this->proptags['startdate']] = $nextOccurrence[$this->proptags['startdate']];
165
-					$this->action['commonstart'] = $props[$this->proptags['commonstart']] = $nextOccurrence[$this->proptags['startdate']];
166
-
167
-					// If current task as been mark as 'Complete' then next occurrence should be incomplete.
168
-					if (isset($this->action['complete']) && $this->action['complete'] == 1) {
169
-						$this->action['status'] = $props[$this->proptags["status"]] = olTaskNotStarted;
170
-						$this->action['complete'] = $props[$this->proptags["complete"]] = false;
171
-						$this->action['percent_complete'] = $props[$this->proptags["percent_complete"]] = 0;
172
-					}
173
-
174
-					$props[$this->proptags["dead_occurrence"]] = false;
175
-				}
176
-				else {
177
-					if (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence']) {
178
-						return false;
179
-					}
180
-
181
-					// Didn't get next occurrence, probably this is the last one, so recurrence ends here
182
-					$props[$this->proptags["dead_occurrence"]] = true;
183
-					$props[$this->proptags["datecompleted"]] = $this->action['datecompleted'];
184
-					$props[$this->proptags["task_f_creator"]] = true;
185
-
186
-					// OL props
187
-					$props[$this->proptags["side_effects"]] = 1296;
188
-					$props[$this->proptags["icon_index"]] = 1280;
189
-				}
190
-
191
-				mapi_setprops($this->message, $props);
192
-			}
193
-
194
-			return $result;
195
-		}
196
-
197
-		/**
198
-		 * Function which return properties of next occurrence.
199
-		 *
200
-		 *@return array startdate/enddate of next occurrence
201
-		 */
202
-		public function getNextOccurrence() {
203
-			if ($this->recur) {
204
-				$items = [];
205
-
206
-				// @TODO: fix start of range
207
-				$start = isset($this->messageprops[$this->proptags["duedate"]]) ? $this->messageprops[$this->proptags["duedate"]] : $this->action['start'];
208
-				$dayend = ($this->recur['term'] == 0x23) ? 0x7FFFFFFF : $this->dayStartOf($this->recur["end"]);
209
-
210
-				// Fix recur object
211
-				$this->recur['startocc'] = 0;
212
-				$this->recur['endocc'] = 0;
213
-
214
-				// Retrieve next occurrence
215
-				$items = $this->getItems($start, $dayend, 1);
216
-
217
-				return !empty($items) ? $items[0] : false;
218
-			}
219
-		}
220
-
221
-		/**
222
-		 * Function which clones current occurrence and sets appropriate properties.
223
-		 * The original recurring item is moved to next occurrence.
224
-		 *
225
-		 *@param bool $markComplete true if existing occurrence has to be mark complete else false
226
-		 */
227
-		public function regenerateTask($markComplete) {
228
-			// Get all properties
229
-			$taskItemProps = mapi_getprops($this->message);
230
-
231
-			if (isset($this->action["subject"])) {
232
-				$taskItemProps[$this->proptags["subject"]] = $this->action["subject"];
233
-			}
234
-			if (isset($this->action["importance"])) {
235
-				$taskItemProps[$this->proptags["importance"]] = $this->action["importance"];
236
-			}
237
-			if (isset($this->action["startdate"])) {
238
-				$taskItemProps[$this->proptags["startdate"]] = $this->action["startdate"];
239
-				$taskItemProps[$this->proptags["commonstart"]] = $this->action["startdate"];
240
-			}
241
-			if (isset($this->action["duedate"])) {
242
-				$taskItemProps[$this->proptags["duedate"]] = $this->action["duedate"];
243
-				$taskItemProps[$this->proptags["commonend"]] = $this->action["duedate"];
244
-			}
245
-
246
-			$folder = mapi_msgstore_openentry($this->store, $taskItemProps[PR_PARENT_ENTRYID]);
247
-			$newMessage = mapi_folder_createmessage($folder);
248
-
249
-			$taskItemProps[$this->proptags["status"]] = $markComplete ? olTaskComplete : olTaskNotStarted;
250
-			$taskItemProps[$this->proptags["complete"]] = $markComplete;
251
-			$taskItemProps[$this->proptags["percent_complete"]] = $markComplete ? 1 : 0;
252
-
253
-			// This occurrence has been marked as 'Complete' so disable reminder
254
-			if ($markComplete) {
255
-				$taskItemProps[$this->proptags["reset_reminder"]] = false;
256
-				$taskItemProps[$this->proptags["reminder"]] = false;
257
-				$taskItemProps[$this->proptags["datecompleted"]] = $this->action["datecompleted"];
258
-
259
-				unset($this->action[$this->proptags['datecompleted']]);
260
-			}
261
-
262
-			// Recurrence ends for this item
263
-			$taskItemProps[$this->proptags["dead_occurrence"]] = true;
264
-			$taskItemProps[$this->proptags["task_f_creator"]] = true;
265
-
266
-			// OL props
267
-			$taskItemProps[$this->proptags["side_effects"]] = 1296;
268
-			$taskItemProps[$this->proptags["icon_index"]] = 1280;
269
-
270
-			// Copy recipients
271
-			$recipienttable = mapi_message_getrecipienttable($this->message);
272
-			$recipients = mapi_table_queryallrows($recipienttable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_RECIPIENT_ENTRYID, PR_RECIPIENT_TYPE, PR_SEND_INTERNET_ENCODING, PR_SEND_RICH_INFO, PR_RECIPIENT_DISPLAY_NAME, PR_ADDRTYPE, PR_DISPLAY_TYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TRACKSTATUS_TIME, PR_RECIPIENT_FLAGS, PR_ROWID]);
273
-
274
-			$copy_to_recipientTable = mapi_message_getrecipienttable($newMessage);
275
-			$copy_to_recipientRows = mapi_table_queryallrows($copy_to_recipientTable, [PR_ROWID]);
276
-			foreach ($copy_to_recipientRows as $recipient) {
277
-				mapi_message_modifyrecipients($newMessage, MODRECIP_REMOVE, [$recipient]);
278
-			}
279
-			mapi_message_modifyrecipients($newMessage, MODRECIP_ADD, $recipients);
280
-
281
-			// Copy attachments
282
-			$attachmentTable = mapi_message_getattachmenttable($this->message);
283
-			if ($attachmentTable) {
284
-				$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
285
-
286
-				foreach ($attachments as $attach_props) {
287
-					$attach_old = mapi_message_openattach($this->message, (int) $attach_props[PR_ATTACH_NUM]);
288
-					$attach_newResourceMsg = mapi_message_createattach($newMessage);
289
-
290
-					mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
291
-					mapi_savechanges($attach_newResourceMsg);
292
-				}
293
-			}
294
-
295
-			mapi_setprops($newMessage, $taskItemProps);
296
-			mapi_savechanges($newMessage);
297
-
298
-			// Update body of original message
299
-			$msgbody = mapi_openproperty($this->message, PR_BODY);
300
-			$msgbody = trim($msgbody, "\0");
301
-			$separator = "------------\r\n";
302
-
303
-			if (!empty($msgbody) && strrpos($msgbody, $separator) === false) {
304
-				$msgbody = $separator . $msgbody;
305
-				$stream = mapi_openproperty($this->message, PR_BODY, IID_IStream, STGM_TRANSACTED, 0);
306
-				mapi_stream_setsize($stream, strlen($msgbody));
307
-				mapi_stream_write($stream, $msgbody);
308
-				mapi_stream_commit($stream);
309
-			}
310
-
311
-			// We need these properties to notify client
312
-			return mapi_getprops($newMessage, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
313
-		}
314
-
315
-		/**
316
-		 * processOccurrenceItem, adds an item to a list of occurrences, but only if the
317
-		 * resulting occurrence starts or ends in the interval <$start, $end>.
318
-		 *
319
-		 * @param array $items    reference to the array to be added to
320
-		 * @param date  $start    start of timeframe in GMT TIME
321
-		 * @param date  $end      end of timeframe in GMT TIME
322
-		 * @param date  $basedate (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
323
-		 * @param mixed $now
324
-		 */
325
-		public function processOccurrenceItem(&$items, $start, $end, $now) {
326
-			if ($now > $start) {
327
-				$newItem = [];
328
-				$newItem[$this->proptags['startdate']] = $now;
329
-
330
-				// If startdate and enddate are set on task, then slide enddate according to duration
331
-				if (isset($this->messageprops[$this->proptags["startdate"]], $this->messageprops[$this->proptags["duedate"]])) {
332
-					$newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']] + ($this->messageprops[$this->proptags["duedate"]] - $this->messageprops[$this->proptags["startdate"]]);
333
-				}
334
-				else {
335
-					$newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']];
336
-				}
337
-
338
-				$items[] = $newItem;
339
-			}
340
-		}
341
-
342
-		/**
343
-		 * Function which marks existing occurrence to 'Complete'.
344
-		 *
345
-		 *@param array $recur array action from client
346
-		 *
347
-		 *@return array of properties of regenerated task else false
348
-		 */
349
-		public function markOccurrenceComplete(&$recur) {
350
-			// Fix timezone object
351
-			$this->tz = false;
352
-			$this->action = &$recur;
353
-			$dead_occurrence = isset($this->messageprops[$this->proptags['dead_occurrence']]) ? $this->messageprops[$this->proptags['dead_occurrence']] : false;
354
-
355
-			if (!$dead_occurrence) {
356
-				return $this->moveToNextOccurrence();
357
-			}
358
-
359
-			return false;
360
-		}
361
-
362
-		/**
363
-		 * Function which sets reminder on recurring task after existing occurrence has been deleted or marked complete.
364
-		 *
365
-		 *@param array $nextOccurrence properties of next occurrence
366
-		 */
367
-		public function setReminder($nextOccurrence) {
368
-			$props = [];
369
-			if ($nextOccurrence) {
370
-				// Check if reminder is reset. Default is 'false'
371
-				$reset_reminder = isset($this->messageprops[$this->proptags['reset_reminder']]) ? $this->messageprops[$this->proptags['reset_reminder']] : false;
372
-				$reminder = $this->messageprops[$this->proptags['reminder']];
373
-
374
-				// Either reminder was already set OR reminder was set but was dismissed bty user
375
-				if ($reminder || $reset_reminder) {
376
-					// Reminder can be set at any time either before or after the duedate, so get duration between the reminder time and duedate
377
-					$reminder_time = isset($this->messageprops[$this->proptags['reminder_time']]) ? $this->messageprops[$this->proptags['reminder_time']] : 0;
378
-					$reminder_difference = isset($this->messageprops[$this->proptags['duedate']]) ? $this->messageprops[$this->proptags['duedate']] : 0;
379
-					$reminder_difference = $reminder_difference - $reminder_time;
380
-
381
-					// Apply duration to next calculated duedate
382
-					$next_reminder_time = $nextOccurrence[$this->proptags['duedate']] - $reminder_difference;
383
-
384
-					$props[$this->proptags['reminder_time']] = $next_reminder_time;
385
-					$props[$this->proptags['flagdueby']] = $next_reminder_time;
386
-					$this->action['reminder'] = $props[$this->proptags['reminder']] = true;
387
-				}
388
-			}
389
-			else {
390
-				// Didn't get next occurrence, probably this is the last occurrence
391
-				$props[$this->proptags['reminder']] = false;
392
-				$props[$this->proptags['reset_reminder']] = false;
393
-			}
394
-
395
-			if (!empty($props)) {
396
-				mapi_setprops($this->message, $props);
397
-			}
398
-		}
399
-
400
-		/**
401
-		 * Function which recurring task to next occurrence.
402
-		 * It simply doesn't regenerate task.
403
-		 *
404
-		 * @param array $action
405
-		 */
406
-		public function deleteOccurrence($action) {
407
-			$this->tz = false;
408
-			$this->action = $action;
409
-			$result = $this->moveToNextOccurrence();
410
-
411
-			mapi_savechanges($this->message);
412
-
413
-			return $result;
414
-		}
415
-	}
145
+            if ((empty($this->action['startdate']) && empty($this->action['duedate'])) ||
146
+                ($this->action['complete'] == 1) || (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence'])) {
147
+                $nextOccurrence = $this->getNextOccurrence();
148
+                $result = mapi_getprops($this->message, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
149
+
150
+                $props = [];
151
+                if ($nextOccurrence) {
152
+                    if (!isset($this->action['deleteOccurrence'])) {
153
+                        // Create current occurrence as separate task
154
+                        $result = $this->regenerateTask($this->action['complete']);
155
+                    }
156
+
157
+                    // Set reminder for next occurrence
158
+                    $this->setReminder($nextOccurrence);
159
+
160
+                    // Update properties for next occurrence
161
+                    $this->action['duedate'] = $props[$this->proptags['duedate']] = $nextOccurrence[$this->proptags['duedate']];
162
+                    $this->action['commonend'] = $props[$this->proptags['commonend']] = $nextOccurrence[$this->proptags['duedate']];
163
+
164
+                    $this->action['startdate'] = $props[$this->proptags['startdate']] = $nextOccurrence[$this->proptags['startdate']];
165
+                    $this->action['commonstart'] = $props[$this->proptags['commonstart']] = $nextOccurrence[$this->proptags['startdate']];
166
+
167
+                    // If current task as been mark as 'Complete' then next occurrence should be incomplete.
168
+                    if (isset($this->action['complete']) && $this->action['complete'] == 1) {
169
+                        $this->action['status'] = $props[$this->proptags["status"]] = olTaskNotStarted;
170
+                        $this->action['complete'] = $props[$this->proptags["complete"]] = false;
171
+                        $this->action['percent_complete'] = $props[$this->proptags["percent_complete"]] = 0;
172
+                    }
173
+
174
+                    $props[$this->proptags["dead_occurrence"]] = false;
175
+                }
176
+                else {
177
+                    if (isset($this->action['deleteOccurrence']) && $this->action['deleteOccurrence']) {
178
+                        return false;
179
+                    }
180
+
181
+                    // Didn't get next occurrence, probably this is the last one, so recurrence ends here
182
+                    $props[$this->proptags["dead_occurrence"]] = true;
183
+                    $props[$this->proptags["datecompleted"]] = $this->action['datecompleted'];
184
+                    $props[$this->proptags["task_f_creator"]] = true;
185
+
186
+                    // OL props
187
+                    $props[$this->proptags["side_effects"]] = 1296;
188
+                    $props[$this->proptags["icon_index"]] = 1280;
189
+                }
190
+
191
+                mapi_setprops($this->message, $props);
192
+            }
193
+
194
+            return $result;
195
+        }
196
+
197
+        /**
198
+         * Function which return properties of next occurrence.
199
+         *
200
+         *@return array startdate/enddate of next occurrence
201
+         */
202
+        public function getNextOccurrence() {
203
+            if ($this->recur) {
204
+                $items = [];
205
+
206
+                // @TODO: fix start of range
207
+                $start = isset($this->messageprops[$this->proptags["duedate"]]) ? $this->messageprops[$this->proptags["duedate"]] : $this->action['start'];
208
+                $dayend = ($this->recur['term'] == 0x23) ? 0x7FFFFFFF : $this->dayStartOf($this->recur["end"]);
209
+
210
+                // Fix recur object
211
+                $this->recur['startocc'] = 0;
212
+                $this->recur['endocc'] = 0;
213
+
214
+                // Retrieve next occurrence
215
+                $items = $this->getItems($start, $dayend, 1);
216
+
217
+                return !empty($items) ? $items[0] : false;
218
+            }
219
+        }
220
+
221
+        /**
222
+         * Function which clones current occurrence and sets appropriate properties.
223
+         * The original recurring item is moved to next occurrence.
224
+         *
225
+         *@param bool $markComplete true if existing occurrence has to be mark complete else false
226
+         */
227
+        public function regenerateTask($markComplete) {
228
+            // Get all properties
229
+            $taskItemProps = mapi_getprops($this->message);
230
+
231
+            if (isset($this->action["subject"])) {
232
+                $taskItemProps[$this->proptags["subject"]] = $this->action["subject"];
233
+            }
234
+            if (isset($this->action["importance"])) {
235
+                $taskItemProps[$this->proptags["importance"]] = $this->action["importance"];
236
+            }
237
+            if (isset($this->action["startdate"])) {
238
+                $taskItemProps[$this->proptags["startdate"]] = $this->action["startdate"];
239
+                $taskItemProps[$this->proptags["commonstart"]] = $this->action["startdate"];
240
+            }
241
+            if (isset($this->action["duedate"])) {
242
+                $taskItemProps[$this->proptags["duedate"]] = $this->action["duedate"];
243
+                $taskItemProps[$this->proptags["commonend"]] = $this->action["duedate"];
244
+            }
245
+
246
+            $folder = mapi_msgstore_openentry($this->store, $taskItemProps[PR_PARENT_ENTRYID]);
247
+            $newMessage = mapi_folder_createmessage($folder);
248
+
249
+            $taskItemProps[$this->proptags["status"]] = $markComplete ? olTaskComplete : olTaskNotStarted;
250
+            $taskItemProps[$this->proptags["complete"]] = $markComplete;
251
+            $taskItemProps[$this->proptags["percent_complete"]] = $markComplete ? 1 : 0;
252
+
253
+            // This occurrence has been marked as 'Complete' so disable reminder
254
+            if ($markComplete) {
255
+                $taskItemProps[$this->proptags["reset_reminder"]] = false;
256
+                $taskItemProps[$this->proptags["reminder"]] = false;
257
+                $taskItemProps[$this->proptags["datecompleted"]] = $this->action["datecompleted"];
258
+
259
+                unset($this->action[$this->proptags['datecompleted']]);
260
+            }
261
+
262
+            // Recurrence ends for this item
263
+            $taskItemProps[$this->proptags["dead_occurrence"]] = true;
264
+            $taskItemProps[$this->proptags["task_f_creator"]] = true;
265
+
266
+            // OL props
267
+            $taskItemProps[$this->proptags["side_effects"]] = 1296;
268
+            $taskItemProps[$this->proptags["icon_index"]] = 1280;
269
+
270
+            // Copy recipients
271
+            $recipienttable = mapi_message_getrecipienttable($this->message);
272
+            $recipients = mapi_table_queryallrows($recipienttable, [PR_ENTRYID, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_RECIPIENT_ENTRYID, PR_RECIPIENT_TYPE, PR_SEND_INTERNET_ENCODING, PR_SEND_RICH_INFO, PR_RECIPIENT_DISPLAY_NAME, PR_ADDRTYPE, PR_DISPLAY_TYPE, PR_RECIPIENT_TRACKSTATUS, PR_RECIPIENT_TRACKSTATUS_TIME, PR_RECIPIENT_FLAGS, PR_ROWID]);
273
+
274
+            $copy_to_recipientTable = mapi_message_getrecipienttable($newMessage);
275
+            $copy_to_recipientRows = mapi_table_queryallrows($copy_to_recipientTable, [PR_ROWID]);
276
+            foreach ($copy_to_recipientRows as $recipient) {
277
+                mapi_message_modifyrecipients($newMessage, MODRECIP_REMOVE, [$recipient]);
278
+            }
279
+            mapi_message_modifyrecipients($newMessage, MODRECIP_ADD, $recipients);
280
+
281
+            // Copy attachments
282
+            $attachmentTable = mapi_message_getattachmenttable($this->message);
283
+            if ($attachmentTable) {
284
+                $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
285
+
286
+                foreach ($attachments as $attach_props) {
287
+                    $attach_old = mapi_message_openattach($this->message, (int) $attach_props[PR_ATTACH_NUM]);
288
+                    $attach_newResourceMsg = mapi_message_createattach($newMessage);
289
+
290
+                    mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
291
+                    mapi_savechanges($attach_newResourceMsg);
292
+                }
293
+            }
294
+
295
+            mapi_setprops($newMessage, $taskItemProps);
296
+            mapi_savechanges($newMessage);
297
+
298
+            // Update body of original message
299
+            $msgbody = mapi_openproperty($this->message, PR_BODY);
300
+            $msgbody = trim($msgbody, "\0");
301
+            $separator = "------------\r\n";
302
+
303
+            if (!empty($msgbody) && strrpos($msgbody, $separator) === false) {
304
+                $msgbody = $separator . $msgbody;
305
+                $stream = mapi_openproperty($this->message, PR_BODY, IID_IStream, STGM_TRANSACTED, 0);
306
+                mapi_stream_setsize($stream, strlen($msgbody));
307
+                mapi_stream_write($stream, $msgbody);
308
+                mapi_stream_commit($stream);
309
+            }
310
+
311
+            // We need these properties to notify client
312
+            return mapi_getprops($newMessage, [PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID]);
313
+        }
314
+
315
+        /**
316
+         * processOccurrenceItem, adds an item to a list of occurrences, but only if the
317
+         * resulting occurrence starts or ends in the interval <$start, $end>.
318
+         *
319
+         * @param array $items    reference to the array to be added to
320
+         * @param date  $start    start of timeframe in GMT TIME
321
+         * @param date  $end      end of timeframe in GMT TIME
322
+         * @param date  $basedate (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
323
+         * @param mixed $now
324
+         */
325
+        public function processOccurrenceItem(&$items, $start, $end, $now) {
326
+            if ($now > $start) {
327
+                $newItem = [];
328
+                $newItem[$this->proptags['startdate']] = $now;
329
+
330
+                // If startdate and enddate are set on task, then slide enddate according to duration
331
+                if (isset($this->messageprops[$this->proptags["startdate"]], $this->messageprops[$this->proptags["duedate"]])) {
332
+                    $newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']] + ($this->messageprops[$this->proptags["duedate"]] - $this->messageprops[$this->proptags["startdate"]]);
333
+                }
334
+                else {
335
+                    $newItem[$this->proptags['duedate']] = $newItem[$this->proptags['startdate']];
336
+                }
337
+
338
+                $items[] = $newItem;
339
+            }
340
+        }
341
+
342
+        /**
343
+         * Function which marks existing occurrence to 'Complete'.
344
+         *
345
+         *@param array $recur array action from client
346
+         *
347
+         *@return array of properties of regenerated task else false
348
+         */
349
+        public function markOccurrenceComplete(&$recur) {
350
+            // Fix timezone object
351
+            $this->tz = false;
352
+            $this->action = &$recur;
353
+            $dead_occurrence = isset($this->messageprops[$this->proptags['dead_occurrence']]) ? $this->messageprops[$this->proptags['dead_occurrence']] : false;
354
+
355
+            if (!$dead_occurrence) {
356
+                return $this->moveToNextOccurrence();
357
+            }
358
+
359
+            return false;
360
+        }
361
+
362
+        /**
363
+         * Function which sets reminder on recurring task after existing occurrence has been deleted or marked complete.
364
+         *
365
+         *@param array $nextOccurrence properties of next occurrence
366
+         */
367
+        public function setReminder($nextOccurrence) {
368
+            $props = [];
369
+            if ($nextOccurrence) {
370
+                // Check if reminder is reset. Default is 'false'
371
+                $reset_reminder = isset($this->messageprops[$this->proptags['reset_reminder']]) ? $this->messageprops[$this->proptags['reset_reminder']] : false;
372
+                $reminder = $this->messageprops[$this->proptags['reminder']];
373
+
374
+                // Either reminder was already set OR reminder was set but was dismissed bty user
375
+                if ($reminder || $reset_reminder) {
376
+                    // Reminder can be set at any time either before or after the duedate, so get duration between the reminder time and duedate
377
+                    $reminder_time = isset($this->messageprops[$this->proptags['reminder_time']]) ? $this->messageprops[$this->proptags['reminder_time']] : 0;
378
+                    $reminder_difference = isset($this->messageprops[$this->proptags['duedate']]) ? $this->messageprops[$this->proptags['duedate']] : 0;
379
+                    $reminder_difference = $reminder_difference - $reminder_time;
380
+
381
+                    // Apply duration to next calculated duedate
382
+                    $next_reminder_time = $nextOccurrence[$this->proptags['duedate']] - $reminder_difference;
383
+
384
+                    $props[$this->proptags['reminder_time']] = $next_reminder_time;
385
+                    $props[$this->proptags['flagdueby']] = $next_reminder_time;
386
+                    $this->action['reminder'] = $props[$this->proptags['reminder']] = true;
387
+                }
388
+            }
389
+            else {
390
+                // Didn't get next occurrence, probably this is the last occurrence
391
+                $props[$this->proptags['reminder']] = false;
392
+                $props[$this->proptags['reset_reminder']] = false;
393
+            }
394
+
395
+            if (!empty($props)) {
396
+                mapi_setprops($this->message, $props);
397
+            }
398
+        }
399
+
400
+        /**
401
+         * Function which recurring task to next occurrence.
402
+         * It simply doesn't regenerate task.
403
+         *
404
+         * @param array $action
405
+         */
406
+        public function deleteOccurrence($action) {
407
+            $this->tz = false;
408
+            $this->action = $action;
409
+            $result = $this->moveToNextOccurrence();
410
+
411
+            mapi_savechanges($this->message);
412
+
413
+            return $result;
414
+        }
415
+    }
Please login to merge, or discard this patch.
mapi/mapicode.php 1 patch
Indentation   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -27,7 +27,7 @@  discard block
 block discarded – undo
27 27
  * @param mixed $code
28 28
  */
29 29
 function make_mapi_e($code) {
30
-	return (int) mapi_make_scode(1, $code);
30
+    return (int) mapi_make_scode(1, $code);
31 31
 }
32 32
 
33 33
 /**
@@ -36,7 +36,7 @@  discard block
 block discarded – undo
36 36
  * @param mixed $code
37 37
  */
38 38
 function make_mapi_s($code) {
39
-	return (int) mapi_make_scode(0, $code);
39
+    return (int) mapi_make_scode(0, $code);
40 40
 }
41 41
 
42 42
 /* From mapicode.h */
Please login to merge, or discard this patch.
mapi/class.recurrence.php 1 patch
Indentation   +1267 added lines, -1267 removed lines patch added patch discarded remove patch
@@ -5,11 +5,11 @@  discard block
 block discarded – undo
5 5
  * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
6 6
  */
7 7
 
8
-	/**
9
-	 * Recurrence.
10
-	 */
11
-	class Recurrence extends BaseRecurrence {
12
-		/*
8
+    /**
9
+     * Recurrence.
10
+     */
11
+    class Recurrence extends BaseRecurrence {
12
+        /*
13 13
 		 * ABOUT TIMEZONES
14 14
 		 *
15 15
 		 * Timezones are rather complicated here so here are some rules to think about:
@@ -21,1279 +21,1279 @@  discard block
 block discarded – undo
21 21
 		 *   always in LOCAL time.
22 22
 		 */
23 23
 
24
-		// All properties for a recipient that are interesting
25
-		public $recipprops = [
26
-			PR_ENTRYID,
27
-			PR_SEARCH_KEY,
28
-			PR_DISPLAY_NAME,
29
-			PR_EMAIL_ADDRESS,
30
-			PR_RECIPIENT_ENTRYID,
31
-			PR_RECIPIENT_TYPE,
32
-			PR_SEND_INTERNET_ENCODING,
33
-			PR_SEND_RICH_INFO,
34
-			PR_RECIPIENT_DISPLAY_NAME,
35
-			PR_ADDRTYPE,
36
-			PR_DISPLAY_TYPE,
37
-			PR_DISPLAY_TYPE_EX,
38
-			PR_RECIPIENT_TRACKSTATUS,
39
-			PR_RECIPIENT_TRACKSTATUS_TIME,
40
-			PR_RECIPIENT_FLAGS,
41
-			PR_ROWID,
42
-		];
43
-
44
-		/**
45
-		 * Constructor.
46
-		 *
47
-		 * @param resource $store    MAPI Message Store Object
48
-		 * @param resource $message  the MAPI (appointment) message
49
-		 * @param array    $proptags an associative array of protags and their values
50
-		 */
51
-		public function __construct($store, $message, $proptags = []) {
52
-			if ($proptags) {
53
-				$this->proptags = $proptags;
54
-			}
55
-			else {
56
-				$properties = [];
57
-				$properties["entryid"] = PR_ENTRYID;
58
-				$properties["parent_entryid"] = PR_PARENT_ENTRYID;
59
-				$properties["message_class"] = PR_MESSAGE_CLASS;
60
-				$properties["icon_index"] = PR_ICON_INDEX;
61
-				$properties["subject"] = PR_SUBJECT;
62
-				$properties["display_to"] = PR_DISPLAY_TO;
63
-				$properties["importance"] = PR_IMPORTANCE;
64
-				$properties["sensitivity"] = PR_SENSITIVITY;
65
-				$properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d";
66
-				$properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e";
67
-				$properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223";
68
-				$properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:0x8216";
69
-				$properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205";
70
-				$properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
71
-				$properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:0x8215";
72
-				$properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
73
-				$properties["meeting"] = "PT_LONG:PSETID_Appointment:0x8217";
74
-				$properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8235";
75
-				$properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8236";
76
-				$properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:0x8232";
77
-				$properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208";
78
-				$properties["duration"] = "PT_LONG:PSETID_Appointment:0x8213";
79
-				$properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
80
-				$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
81
-				$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
82
-				$properties["recurrencetype"] = "PT_LONG:PSETID_Appointment:0x8231";
83
-				$properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
84
-				$properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
85
-				$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
86
-				$properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502";
87
-				$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
88
-				$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
89
-				$properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228";
90
-				$properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233";
91
-				$properties["timezone"] = "PT_STRING8:PSETID_Appointment:0x8234";
92
-				$properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:0x8560";
93
-				$properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
94
-				$properties["hideattachments"] = "PT_BOOLEAN:PSETID_Common:0x8514";
95
-
96
-				$this->proptags = getPropIdsFromStrings($store, $properties);
97
-			}
98
-
99
-			parent::__construct($store, $message);
100
-		}
101
-
102
-		/**
103
-		 * Create an exception.
104
-		 *
105
-		 * @param array        $exception_props  the exception properties (same properties as normal recurring items)
106
-		 * @param date         $base_date        the base date of the exception (LOCAL time of non-exception occurrence)
107
-		 * @param bool         $delete           true - delete occurrence, false - create new exception or modify existing
108
-		 * @param array        $exception_recips true - delete occurrence, false - create new exception or modify existing
109
-		 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
110
-		 */
111
-		public function createException($exception_props, $base_date, $delete = false, $exception_recips = [], $copy_attach_from = false) {
112
-			$baseday = $this->dayStartOf($base_date);
113
-			$basetime = $baseday + $this->recur["startocc"] * 60;
114
-
115
-			// Remove any pre-existing exception on this base date
116
-			if ($this->isException($baseday)) {
117
-				$this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence).
118
-			}
119
-
120
-			if (!$delete) {
121
-				if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
122
-					return false;
123
-				}
124
-				// Properties in the attachment are the properties of the base object, plus $exception_props plus the base date
125
-				foreach (["subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus"] as $propname) {
126
-					if (isset($this->messageprops[$this->proptags[$propname]])) {
127
-						$props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]];
128
-					}
129
-				}
130
-
131
-				$props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
132
-				unset($exception_props[PR_MESSAGE_CLASS], $exception_props[PR_ICON_INDEX]);
133
-
134
-				$props = $exception_props + $props;
135
-
136
-				// Basedate in the exception attachment is the GMT time at which the original occurrence would have been
137
-				$props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime);
138
-
139
-				if (!isset($exception_props[$this->proptags["startdate"]])) {
140
-					$props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date);
141
-				}
142
-
143
-				if (!isset($exception_props[$this->proptags["duedate"]])) {
144
-					$props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date);
145
-				}
146
-
147
-				// synchronize commonstart/commonend with startdate/duedate
148
-				if (isset($props[$this->proptags["startdate"]])) {
149
-					$props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]];
150
-				}
151
-
152
-				if (isset($props[$this->proptags["duedate"]])) {
153
-					$props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]];
154
-				}
155
-
156
-				// Save the data into an attachment
157
-				$this->createExceptionAttachment($props, $exception_recips, $copy_attach_from);
158
-
159
-				$changed_item = [];
160
-
161
-				$changed_item["basedate"] = $baseday;
162
-				$changed_item["start"] = $this->fromGMT($this->tz, $props[$this->proptags["startdate"]]);
163
-				$changed_item["end"] = $this->fromGMT($this->tz, $props[$this->proptags["duedate"]]);
164
-
165
-				if (array_key_exists($this->proptags["subject"], $exception_props)) {
166
-					$changed_item["subject"] = $exception_props[$this->proptags["subject"]];
167
-				}
168
-
169
-				if (array_key_exists($this->proptags["location"], $exception_props)) {
170
-					$changed_item["location"] = $exception_props[$this->proptags["location"]];
171
-				}
172
-
173
-				if (array_key_exists($this->proptags["label"], $exception_props)) {
174
-					$changed_item["label"] = $exception_props[$this->proptags["label"]];
175
-				}
176
-
177
-				if (array_key_exists($this->proptags["reminder"], $exception_props)) {
178
-					$changed_item["reminder_set"] = $exception_props[$this->proptags["reminder"]];
179
-				}
180
-
181
-				if (array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
182
-					$changed_item["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
183
-				}
184
-
185
-				if (array_key_exists($this->proptags["alldayevent"], $exception_props)) {
186
-					$changed_item["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
187
-				}
188
-
189
-				if (array_key_exists($this->proptags["busystatus"], $exception_props)) {
190
-					$changed_item["busystatus"] = $exception_props[$this->proptags["busystatus"]];
191
-				}
192
-
193
-				// Add the changed occurrence to the list
194
-				array_push($this->recur["changed_occurrences"], $changed_item);
195
-			}
196
-			else {
197
-				// Delete the occurrence by placing it in the deleted occurrences list
198
-				array_push($this->recur["deleted_occurrences"], $baseday);
199
-			}
200
-
201
-			// Turn on hideattachments, because the attachments in this item are the exceptions
202
-			mapi_setprops($this->message, [$this->proptags["hideattachments"] => true]);
203
-
204
-			// Save recurrence data to message
205
-			$this->saveRecurrence();
206
-
207
-			return true;
208
-		}
209
-
210
-		/**
211
-		 * Modifies an existing exception, but only updates the given properties
212
-		 * NOTE: You can't remove properties from an exception, only add new ones.
213
-		 *
214
-		 * @param mixed $exception_props
215
-		 * @param mixed $base_date
216
-		 * @param mixed $exception_recips
217
-		 * @param mixed $copy_attach_from
218
-		 */
219
-		public function modifyException($exception_props, $base_date, $exception_recips = [], $copy_attach_from = false) {
220
-			if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
221
-				return false;
222
-			}
223
-
224
-			$baseday = $this->dayStartOf($base_date);
225
-			$extomodify = false;
226
-
227
-			for ($i = 0, $len = count($this->recur["changed_occurrences"]); $i < $len; ++$i) {
228
-				if ($this->isSameDay($this->recur["changed_occurrences"][$i]["basedate"], $baseday)) {
229
-					$extomodify = &$this->recur["changed_occurrences"][$i];
230
-				}
231
-			}
232
-
233
-			if (!$extomodify) {
234
-				return false;
235
-			}
236
-
237
-			// remove basedate property as we want to preserve the old value
238
-			// client will send basedate with time part as zero, so discard that value
239
-			unset($exception_props[$this->proptags["basedate"]]);
240
-
241
-			if (array_key_exists($this->proptags["startdate"], $exception_props)) {
242
-				$extomodify["start"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
243
-			}
244
-
245
-			if (array_key_exists($this->proptags["duedate"], $exception_props)) {
246
-				$extomodify["end"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
247
-			}
248
-
249
-			if (array_key_exists($this->proptags["subject"], $exception_props)) {
250
-				$extomodify["subject"] = $exception_props[$this->proptags["subject"]];
251
-			}
252
-
253
-			if (array_key_exists($this->proptags["location"], $exception_props)) {
254
-				$extomodify["location"] = $exception_props[$this->proptags["location"]];
255
-			}
256
-
257
-			if (array_key_exists($this->proptags["label"], $exception_props)) {
258
-				$extomodify["label"] = $exception_props[$this->proptags["label"]];
259
-			}
260
-
261
-			if (array_key_exists($this->proptags["reminder"], $exception_props)) {
262
-				$extomodify["reminder_set"] = $exception_props[$this->proptags["reminder"]];
263
-			}
264
-
265
-			if (array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
266
-				$extomodify["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
267
-			}
268
-
269
-			if (array_key_exists($this->proptags["alldayevent"], $exception_props)) {
270
-				$extomodify["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
271
-			}
272
-
273
-			if (array_key_exists($this->proptags["busystatus"], $exception_props)) {
274
-				$extomodify["busystatus"] = $exception_props[$this->proptags["busystatus"]];
275
-			}
276
-
277
-			$exception_props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
278
-
279
-			// synchronize commonstart/commonend with startdate/duedate
280
-			if (isset($exception_props[$this->proptags["startdate"]])) {
281
-				$exception_props[$this->proptags["commonstart"]] = $exception_props[$this->proptags["startdate"]];
282
-			}
283
-
284
-			if (isset($exception_props[$this->proptags["duedate"]])) {
285
-				$exception_props[$this->proptags["commonend"]] = $exception_props[$this->proptags["duedate"]];
286
-			}
287
-
288
-			$attach = $this->getExceptionAttachment($baseday);
289
-			if (!$attach) {
290
-				if ($copy_attach_from) {
291
-					$this->deleteExceptionAttachment($base_date);
292
-					$this->createException($exception_props, $base_date, false, $exception_recips, $copy_attach_from);
293
-				}
294
-				else {
295
-					$this->createExceptionAttachment($exception_props, $exception_recips, $copy_attach_from);
296
-				}
297
-			}
298
-			else {
299
-				$message = mapi_attach_openobj($attach, MAPI_MODIFY);
300
-
301
-				// Set exception properties on embedded message and save
302
-				mapi_setprops($message, $exception_props);
303
-				$this->setExceptionRecipients($message, $exception_recips, false);
304
-				mapi_savechanges($message);
305
-
306
-				// If a new start or duedate is provided, we update the properties 'PR_EXCEPTION_STARTTIME' and 'PR_EXCEPTION_ENDTIME'
307
-				// on the attachment which holds the embedded msg and save everything.
308
-				$props = [];
309
-				if (isset($exception_props[$this->proptags["startdate"]])) {
310
-					$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
311
-				}
312
-				if (isset($exception_props[$this->proptags["duedate"]])) {
313
-					$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
314
-				}
315
-				if (!empty($props)) {
316
-					mapi_setprops($attach, $props);
317
-				}
318
-
319
-				mapi_savechanges($attach);
320
-			}
321
-
322
-			// Save recurrence data to message
323
-			$this->saveRecurrence();
324
-
325
-			return true;
326
-		}
327
-
328
-		// Checks to see if the following is true:
329
-		// 1) The exception to be created doesn't create two exceptions starting on one day (however, they can END on the same day by modifying duration)
330
-		// 2) The exception to be created doesn't 'jump' over another occurrence (which may be an exception itself!)
331
-		//
332
-		// Both $basedate and $start are in LOCAL time
333
-		public function isValidExceptionDate($basedate, $start) {
334
-			// The way we do this is to look at the days that we're 'moving' the item in the exception. Each
335
-			// of these days may only contain the item that we're modifying. Any other item violates the rules.
336
-
337
-			if ($this->isException($basedate)) {
338
-				// If we're modifying an exception, we want to look at the days that we're 'moving' compared to where
339
-				// the exception used to be.
340
-				$oldexception = $this->getChangeException($basedate);
341
-				$prevday = $this->dayStartOf($oldexception["start"]);
342
-			}
343
-			else {
344
-				// If its a new exception, we want to look at the original placement of this item.
345
-				$prevday = $basedate;
346
-			}
347
-
348
-			$startday = $this->dayStartOf($start);
349
-
350
-			// Get all the occurrences on the days between the basedate (may be reversed)
351
-			if ($prevday < $startday) {
352
-				$items = $this->getItems($this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
353
-			}
354
-			else {
355
-				$items = $this->getItems($this->toGMT($this->tz, $startday), $this->toGMT($this->tz, $prevday + 24 * 60 * 60));
356
-			}
357
-
358
-			// There should now be exactly one item, namely the item that we are modifying. If there are any other items in the range,
359
-			// then we abort the change, since one of the rules has been violated.
360
-			return count($items) == 1;
361
-		}
362
-
363
-		/**
364
-		 * Check to see if the exception proposed at a certain basedate is allowed concerning reminder times:.
365
-		 *
366
-		 * Both must be true:
367
-		 * - reminder time of this item is not before the starttime of the previous recurring item
368
-		 * - reminder time of the next item is not before the starttime of this item
369
-		 *
370
-		 * @param date   $basedate        the base date of the exception (LOCAL time of non-exception occurrence)
371
-		 * @param string $reminderminutes reminder minutes which is set of the item
372
-		 * @param date   $startdate       the startdate of the selected item
373
-		 * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE)
374
-		 */
375
-		public function isValidReminderTime($basedate, $reminderminutes, $startdate) {
376
-			// get all occurrence items before the seleceted items occurrence starttime
377
-			$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate));
378
-
379
-			if (!empty($occitems)) {
380
-				// as occitems array is sorted in ascending order of startdate, to get the previous occurrence we take the last items in occitems .
381
-				$previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]];
382
-
383
-				// if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed
384
-				if ($startdate - ($reminderminutes * 60) <= $previousitem_startdate) {
385
-					return false;
386
-				}
387
-			}
388
-
389
-			// Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence)
390
-			$currentOcc = $this->getItems($this->toGMT($this->tz, $basedate), 0x7FF00000, 2, true);
391
-
392
-			// If there are another two occurrences, then the first is the current occurrence, and the one after that
393
-			// is the next occurrence.
394
-			if (count($currentOcc) > 1) {
395
-				$next = $currentOcc[1];
396
-				// Get reminder time of the next occurrence.
397
-				$nextOccReminderTime = $next[$this->proptags["startdate"]] - ($next[$this->proptags["reminder_minutes"]] * 60);
398
-				// If the reminder time of the next item is before the start of this item, then that's not allowed
399
-				if ($nextOccReminderTime <= $startdate) {
400
-					return false;
401
-				}
402
-			}
403
-
404
-			// All was ok
405
-			return true;
406
-		}
407
-
408
-		public function setRecurrence($tz, $recur) {
409
-			// only reset timezone if specified
410
-			if ($tz) {
411
-				$this->tz = $tz;
412
-			}
413
-
414
-			$this->recur = $recur;
415
-
416
-			if (!isset($this->recur["changed_occurrences"])) {
417
-				$this->recur["changed_occurrences"] = [];
418
-			}
419
-
420
-			if (!isset($this->recur["deleted_occurrences"])) {
421
-				$this->recur["deleted_occurrences"] = [];
422
-			}
423
-
424
-			$this->deleteAttachments();
425
-			$this->saveRecurrence();
426
-
427
-			// if client has not set the recurring_pattern then we should generate it and save it
428
-			$messageProps = mapi_getprops($this->message, [$this->proptags["recurring_pattern"]]);
429
-			if (empty($messageProps[$this->proptags["recurring_pattern"]])) {
430
-				$this->saveRecurrencePattern();
431
-			}
432
-		}
433
-
434
-		// Returns the start or end time of the occurrence on the given base date.
435
-		// This assumes that the basedate you supply is in LOCAL time
436
-		public function getOccurrenceStart($basedate) {
437
-			$daystart = $this->dayStartOf($basedate);
438
-
439
-			return $this->toGMT($this->tz, $daystart + $this->recur["startocc"] * 60);
440
-		}
441
-
442
-		public function getOccurrenceEnd($basedate) {
443
-			$daystart = $this->dayStartOf($basedate);
444
-
445
-			return $this->toGMT($this->tz, $daystart + $this->recur["endocc"] * 60);
446
-		}
447
-
448
-		// Backwards compatible code
449
-		public function getOccurrenceStart($basedate) {
450
-			return $this->getOccurrenceStart($basedate);
451
-		}
452
-
453
-		public function getOccurrenceEnd($basedate) {
454
-			return $this->getOccurrenceEnd($basedate);
455
-		}
456
-
457
-		/**
458
-		 * This function returns the next remindertime starting from $timestamp
459
-		 * When no next reminder exists, false is returned.
460
-		 *
461
-		 * Note: Before saving this new reminder time (when snoozing), you must check for
462
-		 *       yourself if this reminder time is earlier than your snooze time, else
463
-		 *       use your snooze time and not this reminder time.
464
-		 *
465
-		 * @param mixed $timestamp
466
-		 */
467
-		public function getNextReminderTime($timestamp) {
468
-			/**
469
-			 * Get next item from now until forever, but max 1 item with reminder set
470
-			 * Note 0x7ff00000 instead of 0x7fffffff because of possible overflow failures when converting to GMT....
471
-			 * Here for getting next 10 occurrences assuming that next here we will be able to find
472
-			 * nextreminder occurrence in 10 occureneces.
473
-			 */
474
-			$items = $this->getItems($timestamp, 0x7FF00000, 10, true);
475
-
476
-			// Initially setting nextreminder to false so when no next reminder exists, false is returned.
477
-			$nextreminder = false;
478
-			/*
24
+        // All properties for a recipient that are interesting
25
+        public $recipprops = [
26
+            PR_ENTRYID,
27
+            PR_SEARCH_KEY,
28
+            PR_DISPLAY_NAME,
29
+            PR_EMAIL_ADDRESS,
30
+            PR_RECIPIENT_ENTRYID,
31
+            PR_RECIPIENT_TYPE,
32
+            PR_SEND_INTERNET_ENCODING,
33
+            PR_SEND_RICH_INFO,
34
+            PR_RECIPIENT_DISPLAY_NAME,
35
+            PR_ADDRTYPE,
36
+            PR_DISPLAY_TYPE,
37
+            PR_DISPLAY_TYPE_EX,
38
+            PR_RECIPIENT_TRACKSTATUS,
39
+            PR_RECIPIENT_TRACKSTATUS_TIME,
40
+            PR_RECIPIENT_FLAGS,
41
+            PR_ROWID,
42
+        ];
43
+
44
+        /**
45
+         * Constructor.
46
+         *
47
+         * @param resource $store    MAPI Message Store Object
48
+         * @param resource $message  the MAPI (appointment) message
49
+         * @param array    $proptags an associative array of protags and their values
50
+         */
51
+        public function __construct($store, $message, $proptags = []) {
52
+            if ($proptags) {
53
+                $this->proptags = $proptags;
54
+            }
55
+            else {
56
+                $properties = [];
57
+                $properties["entryid"] = PR_ENTRYID;
58
+                $properties["parent_entryid"] = PR_PARENT_ENTRYID;
59
+                $properties["message_class"] = PR_MESSAGE_CLASS;
60
+                $properties["icon_index"] = PR_ICON_INDEX;
61
+                $properties["subject"] = PR_SUBJECT;
62
+                $properties["display_to"] = PR_DISPLAY_TO;
63
+                $properties["importance"] = PR_IMPORTANCE;
64
+                $properties["sensitivity"] = PR_SENSITIVITY;
65
+                $properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d";
66
+                $properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e";
67
+                $properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223";
68
+                $properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:0x8216";
69
+                $properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205";
70
+                $properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
71
+                $properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:0x8215";
72
+                $properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
73
+                $properties["meeting"] = "PT_LONG:PSETID_Appointment:0x8217";
74
+                $properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8235";
75
+                $properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8236";
76
+                $properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:0x8232";
77
+                $properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208";
78
+                $properties["duration"] = "PT_LONG:PSETID_Appointment:0x8213";
79
+                $properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
80
+                $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
81
+                $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
82
+                $properties["recurrencetype"] = "PT_LONG:PSETID_Appointment:0x8231";
83
+                $properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
84
+                $properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
85
+                $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
86
+                $properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502";
87
+                $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
88
+                $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
89
+                $properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228";
90
+                $properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233";
91
+                $properties["timezone"] = "PT_STRING8:PSETID_Appointment:0x8234";
92
+                $properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:0x8560";
93
+                $properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
94
+                $properties["hideattachments"] = "PT_BOOLEAN:PSETID_Common:0x8514";
95
+
96
+                $this->proptags = getPropIdsFromStrings($store, $properties);
97
+            }
98
+
99
+            parent::__construct($store, $message);
100
+        }
101
+
102
+        /**
103
+         * Create an exception.
104
+         *
105
+         * @param array        $exception_props  the exception properties (same properties as normal recurring items)
106
+         * @param date         $base_date        the base date of the exception (LOCAL time of non-exception occurrence)
107
+         * @param bool         $delete           true - delete occurrence, false - create new exception or modify existing
108
+         * @param array        $exception_recips true - delete occurrence, false - create new exception or modify existing
109
+         * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
110
+         */
111
+        public function createException($exception_props, $base_date, $delete = false, $exception_recips = [], $copy_attach_from = false) {
112
+            $baseday = $this->dayStartOf($base_date);
113
+            $basetime = $baseday + $this->recur["startocc"] * 60;
114
+
115
+            // Remove any pre-existing exception on this base date
116
+            if ($this->isException($baseday)) {
117
+                $this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence).
118
+            }
119
+
120
+            if (!$delete) {
121
+                if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
122
+                    return false;
123
+                }
124
+                // Properties in the attachment are the properties of the base object, plus $exception_props plus the base date
125
+                foreach (["subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus"] as $propname) {
126
+                    if (isset($this->messageprops[$this->proptags[$propname]])) {
127
+                        $props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]];
128
+                    }
129
+                }
130
+
131
+                $props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
132
+                unset($exception_props[PR_MESSAGE_CLASS], $exception_props[PR_ICON_INDEX]);
133
+
134
+                $props = $exception_props + $props;
135
+
136
+                // Basedate in the exception attachment is the GMT time at which the original occurrence would have been
137
+                $props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime);
138
+
139
+                if (!isset($exception_props[$this->proptags["startdate"]])) {
140
+                    $props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date);
141
+                }
142
+
143
+                if (!isset($exception_props[$this->proptags["duedate"]])) {
144
+                    $props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date);
145
+                }
146
+
147
+                // synchronize commonstart/commonend with startdate/duedate
148
+                if (isset($props[$this->proptags["startdate"]])) {
149
+                    $props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]];
150
+                }
151
+
152
+                if (isset($props[$this->proptags["duedate"]])) {
153
+                    $props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]];
154
+                }
155
+
156
+                // Save the data into an attachment
157
+                $this->createExceptionAttachment($props, $exception_recips, $copy_attach_from);
158
+
159
+                $changed_item = [];
160
+
161
+                $changed_item["basedate"] = $baseday;
162
+                $changed_item["start"] = $this->fromGMT($this->tz, $props[$this->proptags["startdate"]]);
163
+                $changed_item["end"] = $this->fromGMT($this->tz, $props[$this->proptags["duedate"]]);
164
+
165
+                if (array_key_exists($this->proptags["subject"], $exception_props)) {
166
+                    $changed_item["subject"] = $exception_props[$this->proptags["subject"]];
167
+                }
168
+
169
+                if (array_key_exists($this->proptags["location"], $exception_props)) {
170
+                    $changed_item["location"] = $exception_props[$this->proptags["location"]];
171
+                }
172
+
173
+                if (array_key_exists($this->proptags["label"], $exception_props)) {
174
+                    $changed_item["label"] = $exception_props[$this->proptags["label"]];
175
+                }
176
+
177
+                if (array_key_exists($this->proptags["reminder"], $exception_props)) {
178
+                    $changed_item["reminder_set"] = $exception_props[$this->proptags["reminder"]];
179
+                }
180
+
181
+                if (array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
182
+                    $changed_item["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
183
+                }
184
+
185
+                if (array_key_exists($this->proptags["alldayevent"], $exception_props)) {
186
+                    $changed_item["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
187
+                }
188
+
189
+                if (array_key_exists($this->proptags["busystatus"], $exception_props)) {
190
+                    $changed_item["busystatus"] = $exception_props[$this->proptags["busystatus"]];
191
+                }
192
+
193
+                // Add the changed occurrence to the list
194
+                array_push($this->recur["changed_occurrences"], $changed_item);
195
+            }
196
+            else {
197
+                // Delete the occurrence by placing it in the deleted occurrences list
198
+                array_push($this->recur["deleted_occurrences"], $baseday);
199
+            }
200
+
201
+            // Turn on hideattachments, because the attachments in this item are the exceptions
202
+            mapi_setprops($this->message, [$this->proptags["hideattachments"] => true]);
203
+
204
+            // Save recurrence data to message
205
+            $this->saveRecurrence();
206
+
207
+            return true;
208
+        }
209
+
210
+        /**
211
+         * Modifies an existing exception, but only updates the given properties
212
+         * NOTE: You can't remove properties from an exception, only add new ones.
213
+         *
214
+         * @param mixed $exception_props
215
+         * @param mixed $base_date
216
+         * @param mixed $exception_recips
217
+         * @param mixed $copy_attach_from
218
+         */
219
+        public function modifyException($exception_props, $base_date, $exception_recips = [], $copy_attach_from = false) {
220
+            if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
221
+                return false;
222
+            }
223
+
224
+            $baseday = $this->dayStartOf($base_date);
225
+            $extomodify = false;
226
+
227
+            for ($i = 0, $len = count($this->recur["changed_occurrences"]); $i < $len; ++$i) {
228
+                if ($this->isSameDay($this->recur["changed_occurrences"][$i]["basedate"], $baseday)) {
229
+                    $extomodify = &$this->recur["changed_occurrences"][$i];
230
+                }
231
+            }
232
+
233
+            if (!$extomodify) {
234
+                return false;
235
+            }
236
+
237
+            // remove basedate property as we want to preserve the old value
238
+            // client will send basedate with time part as zero, so discard that value
239
+            unset($exception_props[$this->proptags["basedate"]]);
240
+
241
+            if (array_key_exists($this->proptags["startdate"], $exception_props)) {
242
+                $extomodify["start"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
243
+            }
244
+
245
+            if (array_key_exists($this->proptags["duedate"], $exception_props)) {
246
+                $extomodify["end"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
247
+            }
248
+
249
+            if (array_key_exists($this->proptags["subject"], $exception_props)) {
250
+                $extomodify["subject"] = $exception_props[$this->proptags["subject"]];
251
+            }
252
+
253
+            if (array_key_exists($this->proptags["location"], $exception_props)) {
254
+                $extomodify["location"] = $exception_props[$this->proptags["location"]];
255
+            }
256
+
257
+            if (array_key_exists($this->proptags["label"], $exception_props)) {
258
+                $extomodify["label"] = $exception_props[$this->proptags["label"]];
259
+            }
260
+
261
+            if (array_key_exists($this->proptags["reminder"], $exception_props)) {
262
+                $extomodify["reminder_set"] = $exception_props[$this->proptags["reminder"]];
263
+            }
264
+
265
+            if (array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
266
+                $extomodify["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
267
+            }
268
+
269
+            if (array_key_exists($this->proptags["alldayevent"], $exception_props)) {
270
+                $extomodify["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
271
+            }
272
+
273
+            if (array_key_exists($this->proptags["busystatus"], $exception_props)) {
274
+                $extomodify["busystatus"] = $exception_props[$this->proptags["busystatus"]];
275
+            }
276
+
277
+            $exception_props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
278
+
279
+            // synchronize commonstart/commonend with startdate/duedate
280
+            if (isset($exception_props[$this->proptags["startdate"]])) {
281
+                $exception_props[$this->proptags["commonstart"]] = $exception_props[$this->proptags["startdate"]];
282
+            }
283
+
284
+            if (isset($exception_props[$this->proptags["duedate"]])) {
285
+                $exception_props[$this->proptags["commonend"]] = $exception_props[$this->proptags["duedate"]];
286
+            }
287
+
288
+            $attach = $this->getExceptionAttachment($baseday);
289
+            if (!$attach) {
290
+                if ($copy_attach_from) {
291
+                    $this->deleteExceptionAttachment($base_date);
292
+                    $this->createException($exception_props, $base_date, false, $exception_recips, $copy_attach_from);
293
+                }
294
+                else {
295
+                    $this->createExceptionAttachment($exception_props, $exception_recips, $copy_attach_from);
296
+                }
297
+            }
298
+            else {
299
+                $message = mapi_attach_openobj($attach, MAPI_MODIFY);
300
+
301
+                // Set exception properties on embedded message and save
302
+                mapi_setprops($message, $exception_props);
303
+                $this->setExceptionRecipients($message, $exception_recips, false);
304
+                mapi_savechanges($message);
305
+
306
+                // If a new start or duedate is provided, we update the properties 'PR_EXCEPTION_STARTTIME' and 'PR_EXCEPTION_ENDTIME'
307
+                // on the attachment which holds the embedded msg and save everything.
308
+                $props = [];
309
+                if (isset($exception_props[$this->proptags["startdate"]])) {
310
+                    $props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
311
+                }
312
+                if (isset($exception_props[$this->proptags["duedate"]])) {
313
+                    $props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
314
+                }
315
+                if (!empty($props)) {
316
+                    mapi_setprops($attach, $props);
317
+                }
318
+
319
+                mapi_savechanges($attach);
320
+            }
321
+
322
+            // Save recurrence data to message
323
+            $this->saveRecurrence();
324
+
325
+            return true;
326
+        }
327
+
328
+        // Checks to see if the following is true:
329
+        // 1) The exception to be created doesn't create two exceptions starting on one day (however, they can END on the same day by modifying duration)
330
+        // 2) The exception to be created doesn't 'jump' over another occurrence (which may be an exception itself!)
331
+        //
332
+        // Both $basedate and $start are in LOCAL time
333
+        public function isValidExceptionDate($basedate, $start) {
334
+            // The way we do this is to look at the days that we're 'moving' the item in the exception. Each
335
+            // of these days may only contain the item that we're modifying. Any other item violates the rules.
336
+
337
+            if ($this->isException($basedate)) {
338
+                // If we're modifying an exception, we want to look at the days that we're 'moving' compared to where
339
+                // the exception used to be.
340
+                $oldexception = $this->getChangeException($basedate);
341
+                $prevday = $this->dayStartOf($oldexception["start"]);
342
+            }
343
+            else {
344
+                // If its a new exception, we want to look at the original placement of this item.
345
+                $prevday = $basedate;
346
+            }
347
+
348
+            $startday = $this->dayStartOf($start);
349
+
350
+            // Get all the occurrences on the days between the basedate (may be reversed)
351
+            if ($prevday < $startday) {
352
+                $items = $this->getItems($this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
353
+            }
354
+            else {
355
+                $items = $this->getItems($this->toGMT($this->tz, $startday), $this->toGMT($this->tz, $prevday + 24 * 60 * 60));
356
+            }
357
+
358
+            // There should now be exactly one item, namely the item that we are modifying. If there are any other items in the range,
359
+            // then we abort the change, since one of the rules has been violated.
360
+            return count($items) == 1;
361
+        }
362
+
363
+        /**
364
+         * Check to see if the exception proposed at a certain basedate is allowed concerning reminder times:.
365
+         *
366
+         * Both must be true:
367
+         * - reminder time of this item is not before the starttime of the previous recurring item
368
+         * - reminder time of the next item is not before the starttime of this item
369
+         *
370
+         * @param date   $basedate        the base date of the exception (LOCAL time of non-exception occurrence)
371
+         * @param string $reminderminutes reminder minutes which is set of the item
372
+         * @param date   $startdate       the startdate of the selected item
373
+         * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE)
374
+         */
375
+        public function isValidReminderTime($basedate, $reminderminutes, $startdate) {
376
+            // get all occurrence items before the seleceted items occurrence starttime
377
+            $occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate));
378
+
379
+            if (!empty($occitems)) {
380
+                // as occitems array is sorted in ascending order of startdate, to get the previous occurrence we take the last items in occitems .
381
+                $previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]];
382
+
383
+                // if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed
384
+                if ($startdate - ($reminderminutes * 60) <= $previousitem_startdate) {
385
+                    return false;
386
+                }
387
+            }
388
+
389
+            // Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence)
390
+            $currentOcc = $this->getItems($this->toGMT($this->tz, $basedate), 0x7FF00000, 2, true);
391
+
392
+            // If there are another two occurrences, then the first is the current occurrence, and the one after that
393
+            // is the next occurrence.
394
+            if (count($currentOcc) > 1) {
395
+                $next = $currentOcc[1];
396
+                // Get reminder time of the next occurrence.
397
+                $nextOccReminderTime = $next[$this->proptags["startdate"]] - ($next[$this->proptags["reminder_minutes"]] * 60);
398
+                // If the reminder time of the next item is before the start of this item, then that's not allowed
399
+                if ($nextOccReminderTime <= $startdate) {
400
+                    return false;
401
+                }
402
+            }
403
+
404
+            // All was ok
405
+            return true;
406
+        }
407
+
408
+        public function setRecurrence($tz, $recur) {
409
+            // only reset timezone if specified
410
+            if ($tz) {
411
+                $this->tz = $tz;
412
+            }
413
+
414
+            $this->recur = $recur;
415
+
416
+            if (!isset($this->recur["changed_occurrences"])) {
417
+                $this->recur["changed_occurrences"] = [];
418
+            }
419
+
420
+            if (!isset($this->recur["deleted_occurrences"])) {
421
+                $this->recur["deleted_occurrences"] = [];
422
+            }
423
+
424
+            $this->deleteAttachments();
425
+            $this->saveRecurrence();
426
+
427
+            // if client has not set the recurring_pattern then we should generate it and save it
428
+            $messageProps = mapi_getprops($this->message, [$this->proptags["recurring_pattern"]]);
429
+            if (empty($messageProps[$this->proptags["recurring_pattern"]])) {
430
+                $this->saveRecurrencePattern();
431
+            }
432
+        }
433
+
434
+        // Returns the start or end time of the occurrence on the given base date.
435
+        // This assumes that the basedate you supply is in LOCAL time
436
+        public function getOccurrenceStart($basedate) {
437
+            $daystart = $this->dayStartOf($basedate);
438
+
439
+            return $this->toGMT($this->tz, $daystart + $this->recur["startocc"] * 60);
440
+        }
441
+
442
+        public function getOccurrenceEnd($basedate) {
443
+            $daystart = $this->dayStartOf($basedate);
444
+
445
+            return $this->toGMT($this->tz, $daystart + $this->recur["endocc"] * 60);
446
+        }
447
+
448
+        // Backwards compatible code
449
+        public function getOccurrenceStart($basedate) {
450
+            return $this->getOccurrenceStart($basedate);
451
+        }
452
+
453
+        public function getOccurrenceEnd($basedate) {
454
+            return $this->getOccurrenceEnd($basedate);
455
+        }
456
+
457
+        /**
458
+         * This function returns the next remindertime starting from $timestamp
459
+         * When no next reminder exists, false is returned.
460
+         *
461
+         * Note: Before saving this new reminder time (when snoozing), you must check for
462
+         *       yourself if this reminder time is earlier than your snooze time, else
463
+         *       use your snooze time and not this reminder time.
464
+         *
465
+         * @param mixed $timestamp
466
+         */
467
+        public function getNextReminderTime($timestamp) {
468
+            /**
469
+             * Get next item from now until forever, but max 1 item with reminder set
470
+             * Note 0x7ff00000 instead of 0x7fffffff because of possible overflow failures when converting to GMT....
471
+             * Here for getting next 10 occurrences assuming that next here we will be able to find
472
+             * nextreminder occurrence in 10 occureneces.
473
+             */
474
+            $items = $this->getItems($timestamp, 0x7FF00000, 10, true);
475
+
476
+            // Initially setting nextreminder to false so when no next reminder exists, false is returned.
477
+            $nextreminder = false;
478
+            /*
479 479
 			 * Loop through all reminder which we get in items variable
480 480
 			 * and check whether the remindertime is greater than timestamp.
481 481
 			 * On the first occurrence of greater nextreminder break the loop
482 482
 			 * and return the value to calling function.
483 483
 			 */
484
-			for ($i = 0, $len = count($items); $i < $len; ++$i) {
485
-				$item = $items[$i];
486
-				$tempnextreminder = $item[$this->proptags["startdate"]] - ($item[$this->proptags["reminder_minutes"]] * 60);
487
-
488
-				// If tempnextreminder is greater than timestamp then save it in nextreminder and break from the loop.
489
-				if ($tempnextreminder > $timestamp) {
490
-					$nextreminder = $tempnextreminder;
491
-
492
-					break;
493
-				}
494
-			}
495
-
496
-			return $nextreminder;
497
-		}
498
-
499
-		/**
500
-		 * Note: Static function, more like a utility function.
501
-		 *
502
-		 * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are
503
-		 * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval
504
-		 * is <08:00 - 14:00>, the item [6:00 - 8:00> is NOT included, nor is the item [14:00 - 16:00>. However, the item
505
-		 * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>.
506
-		 *
507
-		 * @param $store resource The store in which the calendar resides
508
-		 * @param $calendar resource The calendar to get the items from
509
-		 * @param $viewstart int Timestamp of beginning of view window
510
-		 * @param $viewend int Timestamp of end of view window
511
-		 * @param $propsrequested array Array of properties to return
512
-		 * @param $rows array Array of rowdata as if they were returned directly from mapi_table_queryrows. Each recurring item is
513
-		 *                    expanded so that it seems that there are only many single appointments in the table.
514
-		 */
515
-		public static function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested) {
516
-			return getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested);
517
-		}
518
-
519
-		/*
484
+            for ($i = 0, $len = count($items); $i < $len; ++$i) {
485
+                $item = $items[$i];
486
+                $tempnextreminder = $item[$this->proptags["startdate"]] - ($item[$this->proptags["reminder_minutes"]] * 60);
487
+
488
+                // If tempnextreminder is greater than timestamp then save it in nextreminder and break from the loop.
489
+                if ($tempnextreminder > $timestamp) {
490
+                    $nextreminder = $tempnextreminder;
491
+
492
+                    break;
493
+                }
494
+            }
495
+
496
+            return $nextreminder;
497
+        }
498
+
499
+        /**
500
+         * Note: Static function, more like a utility function.
501
+         *
502
+         * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are
503
+         * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval
504
+         * is <08:00 - 14:00>, the item [6:00 - 8:00> is NOT included, nor is the item [14:00 - 16:00>. However, the item
505
+         * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>.
506
+         *
507
+         * @param $store resource The store in which the calendar resides
508
+         * @param $calendar resource The calendar to get the items from
509
+         * @param $viewstart int Timestamp of beginning of view window
510
+         * @param $viewend int Timestamp of end of view window
511
+         * @param $propsrequested array Array of properties to return
512
+         * @param $rows array Array of rowdata as if they were returned directly from mapi_table_queryrows. Each recurring item is
513
+         *                    expanded so that it seems that there are only many single appointments in the table.
514
+         */
515
+        public static function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested) {
516
+            return getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested);
517
+        }
518
+
519
+        /*
520 520
 		 * CODE BELOW THIS LINE IS FOR INTERNAL USE ONLY
521 521
 		 *****************************************************************************************************************
522 522
 		 */
523 523
 
524
-		/**
525
-		 * Generates and stores recurrence pattern string to recurring_pattern property.
526
-		 */
527
-		public function saveRecurrencePattern() {
528
-			// Start formatting the properties in such a way we can apply
529
-			// them directly into the recurrence pattern.
530
-			$type = $this->recur['type'];
531
-			$everyn = $this->recur['everyn'];
532
-			$start = $this->recur['start'];
533
-			$end = $this->recur['end'];
534
-			$term = $this->recur['term'];
535
-			$numocc = isset($this->recur['numoccur']) ? $this->recur['numoccur'] : false;
536
-			$startocc = $this->recur['startocc'];
537
-			$endocc = $this->recur['endocc'];
538
-			$pattern = '';
539
-			$occSingleDayRank = false;
540
-			$occTimeRange = ($startocc != 0 && $endocc != 0);
541
-
542
-			switch ($type) {
543
-				// Daily
544
-				case 0x0A:
545
-					if ($everyn == 1) {
546
-						$type = _('workday');
547
-						$occSingleDayRank = true;
548
-					}
549
-					elseif ($everyn == (24 * 60)) {
550
-						$type = _('day');
551
-						$occSingleDayRank = true;
552
-					}
553
-					else {
554
-						$everyn /= (24 * 60);
555
-						$type = _('days');
556
-						$occSingleDayRank = false;
557
-					}
558
-
559
-					break;
560
-				// Weekly
561
-				case 0x0B:
562
-					if ($everyn == 1) {
563
-						$type = _('week');
564
-						$occSingleDayRank = true;
565
-					}
566
-					else {
567
-						$type = _('weeks');
568
-						$occSingleDayRank = false;
569
-					}
570
-
571
-					break;
572
-				// Monthly
573
-				case 0x0C:
574
-					if ($everyn == 1) {
575
-						$type = _('month');
576
-						$occSingleDayRank = true;
577
-					}
578
-					else {
579
-						$type = _('months');
580
-						$occSingleDayRank = false;
581
-					}
582
-
583
-					break;
584
-				// Yearly
585
-				case 0x0D:
586
-					if ($everyn <= 12) {
587
-						$everyn = 1;
588
-						$type = _('year');
589
-						$occSingleDayRank = true;
590
-					}
591
-					else {
592
-						$everyn = $everyn / 12;
593
-						$type = _('years');
594
-						$occSingleDayRank = false;
595
-					}
596
-
597
-					break;
598
-			}
599
-
600
-			// get timings of the first occurrence
601
-			$firstoccstartdate = isset($startocc) ? $start + (((int) $startocc) * 60) : $start;
602
-			$firstoccenddate = isset($endocc) ? $end + (((int) $endocc) * 60) : $end;
603
-
604
-			$start = gmdate(_('d-m-Y'), $firstoccstartdate);
605
-			$end = gmdate(_('d-m-Y'), $firstoccenddate);
606
-			$startocc = gmdate(_('G:i'), $firstoccstartdate);
607
-			$endocc = gmdate(_('G:i'), $firstoccenddate);
608
-
609
-			// Based on the properties, we need to generate the recurrence pattern string.
610
-			// This is obviously very easy since we can simply concatenate a bunch of strings,
611
-			// however this messes up translations for languages which order their words
612
-			// differently.
613
-			// To improve translation quality we create a series of default strings, in which
614
-			// we only have to fill in the correct variables. The base string is thus selected
615
-			// based on the available properties.
616
-			if ($term == 0x23) {
617
-				// Never ends
618
-				if ($occTimeRange) {
619
-					if ($occSingleDayRank) {
620
-						$pattern = sprintf(_('Occurs every %s effective %s from %s to %s.'), $type, $start, $startocc, $endocc);
621
-					}
622
-					else {
623
-						$pattern = sprintf(_('Occurs every %s %s effective %s from %s to %s.'), $everyn, $type, $start, $startocc, $endocc);
624
-					}
625
-				}
626
-				else {
627
-					if ($occSingleDayRank) {
628
-						$pattern = sprintf(_('Occurs every %s effective %s.'), $type, $start);
629
-					}
630
-					else {
631
-						$pattern = sprintf(_('Occurs every %s %s effective %s.'), $everyn, $type, $start);
632
-					}
633
-				}
634
-			}
635
-			elseif ($term == 0x22) {
636
-				// After a number of times
637
-				if ($occTimeRange) {
638
-					if ($occSingleDayRank) {
639
-						$pattern = sprintf(ngettext(
640
-							'Occurs every %s effective %s for %s occurrence from %s to %s.',
641
-							'Occurs every %s effective %s for %s occurrences from %s to %s.',
642
-							$numocc
643
-						), $type, $start, $numocc, $startocc, $endocc);
644
-					}
645
-					else {
646
-						$pattern = sprintf(ngettext(
647
-							'Occurs every %s %s effective %s for %s occurrence from %s to %s.',
648
-							'Occurs every %s %s effective %s for %s occurrences %s to %s.',
649
-							$numocc
650
-						), $everyn, $type, $start, $numocc, $startocc, $endocc);
651
-					}
652
-				}
653
-				else {
654
-					if ($occSingleDayRank) {
655
-						$pattern = sprintf(ngettext(
656
-							'Occurs every %s effective %s for %s occurrence.',
657
-							'Occurs every %s effective %s for %s occurrences.',
658
-							$numocc
659
-						), $type, $start, $numocc);
660
-					}
661
-					else {
662
-						$pattern = sprintf(ngettext(
663
-							'Occurs every %s %s effective %s for %s occurrence.',
664
-							'Occurs every %s %s effective %s for %s occurrences.',
665
-							$numocc
666
-						), $everyn, $type, $start, $numocc);
667
-					}
668
-				}
669
-			}
670
-			elseif ($term == 0x21) {
671
-				// After the given enddate
672
-				if ($occTimeRange) {
673
-					if ($occSingleDayRank) {
674
-						$pattern = sprintf(_('Occurs every %s effective %s until %s from %s to %s.'), $type, $start, $end, $startocc, $endocc);
675
-					}
676
-					else {
677
-						$pattern = sprintf(_('Occurs every %s %s effective %s until %s from %s to %s.'), $everyn, $type, $start, $end, $startocc, $endocc);
678
-					}
679
-				}
680
-				else {
681
-					if ($occSingleDayRank) {
682
-						$pattern = sprintf(_('Occurs every %s effective %s until %s.'), $type, $start, $end);
683
-					}
684
-					else {
685
-						$pattern = sprintf(_('Occurs every %s %s effective %s until %s.'), $everyn, $type, $start, $end);
686
-					}
687
-				}
688
-			}
689
-
690
-			if (!empty($pattern)) {
691
-				mapi_setprops($this->message, [$this->proptags["recurring_pattern"] => $pattern]);
692
-			}
693
-		}
694
-
695
-		/*
524
+        /**
525
+         * Generates and stores recurrence pattern string to recurring_pattern property.
526
+         */
527
+        public function saveRecurrencePattern() {
528
+            // Start formatting the properties in such a way we can apply
529
+            // them directly into the recurrence pattern.
530
+            $type = $this->recur['type'];
531
+            $everyn = $this->recur['everyn'];
532
+            $start = $this->recur['start'];
533
+            $end = $this->recur['end'];
534
+            $term = $this->recur['term'];
535
+            $numocc = isset($this->recur['numoccur']) ? $this->recur['numoccur'] : false;
536
+            $startocc = $this->recur['startocc'];
537
+            $endocc = $this->recur['endocc'];
538
+            $pattern = '';
539
+            $occSingleDayRank = false;
540
+            $occTimeRange = ($startocc != 0 && $endocc != 0);
541
+
542
+            switch ($type) {
543
+                // Daily
544
+                case 0x0A:
545
+                    if ($everyn == 1) {
546
+                        $type = _('workday');
547
+                        $occSingleDayRank = true;
548
+                    }
549
+                    elseif ($everyn == (24 * 60)) {
550
+                        $type = _('day');
551
+                        $occSingleDayRank = true;
552
+                    }
553
+                    else {
554
+                        $everyn /= (24 * 60);
555
+                        $type = _('days');
556
+                        $occSingleDayRank = false;
557
+                    }
558
+
559
+                    break;
560
+                // Weekly
561
+                case 0x0B:
562
+                    if ($everyn == 1) {
563
+                        $type = _('week');
564
+                        $occSingleDayRank = true;
565
+                    }
566
+                    else {
567
+                        $type = _('weeks');
568
+                        $occSingleDayRank = false;
569
+                    }
570
+
571
+                    break;
572
+                // Monthly
573
+                case 0x0C:
574
+                    if ($everyn == 1) {
575
+                        $type = _('month');
576
+                        $occSingleDayRank = true;
577
+                    }
578
+                    else {
579
+                        $type = _('months');
580
+                        $occSingleDayRank = false;
581
+                    }
582
+
583
+                    break;
584
+                // Yearly
585
+                case 0x0D:
586
+                    if ($everyn <= 12) {
587
+                        $everyn = 1;
588
+                        $type = _('year');
589
+                        $occSingleDayRank = true;
590
+                    }
591
+                    else {
592
+                        $everyn = $everyn / 12;
593
+                        $type = _('years');
594
+                        $occSingleDayRank = false;
595
+                    }
596
+
597
+                    break;
598
+            }
599
+
600
+            // get timings of the first occurrence
601
+            $firstoccstartdate = isset($startocc) ? $start + (((int) $startocc) * 60) : $start;
602
+            $firstoccenddate = isset($endocc) ? $end + (((int) $endocc) * 60) : $end;
603
+
604
+            $start = gmdate(_('d-m-Y'), $firstoccstartdate);
605
+            $end = gmdate(_('d-m-Y'), $firstoccenddate);
606
+            $startocc = gmdate(_('G:i'), $firstoccstartdate);
607
+            $endocc = gmdate(_('G:i'), $firstoccenddate);
608
+
609
+            // Based on the properties, we need to generate the recurrence pattern string.
610
+            // This is obviously very easy since we can simply concatenate a bunch of strings,
611
+            // however this messes up translations for languages which order their words
612
+            // differently.
613
+            // To improve translation quality we create a series of default strings, in which
614
+            // we only have to fill in the correct variables. The base string is thus selected
615
+            // based on the available properties.
616
+            if ($term == 0x23) {
617
+                // Never ends
618
+                if ($occTimeRange) {
619
+                    if ($occSingleDayRank) {
620
+                        $pattern = sprintf(_('Occurs every %s effective %s from %s to %s.'), $type, $start, $startocc, $endocc);
621
+                    }
622
+                    else {
623
+                        $pattern = sprintf(_('Occurs every %s %s effective %s from %s to %s.'), $everyn, $type, $start, $startocc, $endocc);
624
+                    }
625
+                }
626
+                else {
627
+                    if ($occSingleDayRank) {
628
+                        $pattern = sprintf(_('Occurs every %s effective %s.'), $type, $start);
629
+                    }
630
+                    else {
631
+                        $pattern = sprintf(_('Occurs every %s %s effective %s.'), $everyn, $type, $start);
632
+                    }
633
+                }
634
+            }
635
+            elseif ($term == 0x22) {
636
+                // After a number of times
637
+                if ($occTimeRange) {
638
+                    if ($occSingleDayRank) {
639
+                        $pattern = sprintf(ngettext(
640
+                            'Occurs every %s effective %s for %s occurrence from %s to %s.',
641
+                            'Occurs every %s effective %s for %s occurrences from %s to %s.',
642
+                            $numocc
643
+                        ), $type, $start, $numocc, $startocc, $endocc);
644
+                    }
645
+                    else {
646
+                        $pattern = sprintf(ngettext(
647
+                            'Occurs every %s %s effective %s for %s occurrence from %s to %s.',
648
+                            'Occurs every %s %s effective %s for %s occurrences %s to %s.',
649
+                            $numocc
650
+                        ), $everyn, $type, $start, $numocc, $startocc, $endocc);
651
+                    }
652
+                }
653
+                else {
654
+                    if ($occSingleDayRank) {
655
+                        $pattern = sprintf(ngettext(
656
+                            'Occurs every %s effective %s for %s occurrence.',
657
+                            'Occurs every %s effective %s for %s occurrences.',
658
+                            $numocc
659
+                        ), $type, $start, $numocc);
660
+                    }
661
+                    else {
662
+                        $pattern = sprintf(ngettext(
663
+                            'Occurs every %s %s effective %s for %s occurrence.',
664
+                            'Occurs every %s %s effective %s for %s occurrences.',
665
+                            $numocc
666
+                        ), $everyn, $type, $start, $numocc);
667
+                    }
668
+                }
669
+            }
670
+            elseif ($term == 0x21) {
671
+                // After the given enddate
672
+                if ($occTimeRange) {
673
+                    if ($occSingleDayRank) {
674
+                        $pattern = sprintf(_('Occurs every %s effective %s until %s from %s to %s.'), $type, $start, $end, $startocc, $endocc);
675
+                    }
676
+                    else {
677
+                        $pattern = sprintf(_('Occurs every %s %s effective %s until %s from %s to %s.'), $everyn, $type, $start, $end, $startocc, $endocc);
678
+                    }
679
+                }
680
+                else {
681
+                    if ($occSingleDayRank) {
682
+                        $pattern = sprintf(_('Occurs every %s effective %s until %s.'), $type, $start, $end);
683
+                    }
684
+                    else {
685
+                        $pattern = sprintf(_('Occurs every %s %s effective %s until %s.'), $everyn, $type, $start, $end);
686
+                    }
687
+                }
688
+            }
689
+
690
+            if (!empty($pattern)) {
691
+                mapi_setprops($this->message, [$this->proptags["recurring_pattern"] => $pattern]);
692
+            }
693
+        }
694
+
695
+        /*
696 696
 		 * Remove an exception by base_date. This is the base date in local daystart time
697 697
 		 */
698
-		public function deleteException($base_date) {
699
-			// Remove all exceptions on $base_date from the deleted and changed occurrences lists
700
-
701
-			// Remove all items in $todelete from deleted_occurrences
702
-			$new = [];
703
-
704
-			foreach ($this->recur["deleted_occurrences"] as $entry) {
705
-				if ($entry != $base_date) {
706
-					$new[] = $entry;
707
-				}
708
-			}
709
-			$this->recur["deleted_occurrences"] = $new;
710
-
711
-			$new = [];
712
-
713
-			foreach ($this->recur["changed_occurrences"] as $entry) {
714
-				if (!$this->isSameDay($entry["basedate"], $base_date)) {
715
-					$new[] = $entry;
716
-				}
717
-				else {
718
-					$this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
719
-				}
720
-			}
721
-
722
-			$this->recur["changed_occurrences"] = $new;
723
-		}
724
-
725
-		/**
726
-		 * Function which saves the exception data in an attachment.
727
-		 *
728
-		 * @param array        $exception_props  the exception data (like any other MAPI appointment)
729
-		 * @param array        $exception_recips list of recipients
730
-		 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
731
-		 */
732
-		public function createExceptionAttachment($exception_props, $exception_recips = [], $copy_attach_from = false) {
733
-			// Create new attachment.
734
-			$attachment = mapi_message_createattach($this->message);
735
-			$props = [];
736
-			$props[PR_ATTACHMENT_FLAGS] = 2;
737
-			$props[PR_ATTACHMENT_HIDDEN] = true;
738
-			$props[PR_ATTACHMENT_LINKID] = 0;
739
-			$props[PR_ATTACH_FLAGS] = 0;
740
-			$props[PR_ATTACH_METHOD] = 5;
741
-			$props[PR_DISPLAY_NAME] = "Exception";
742
-			$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
743
-			$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
744
-			mapi_setprops($attachment, $props);
745
-
746
-			$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
747
-
748
-			if ($copy_attach_from) {
749
-				$attachmentTable = mapi_message_getattachmenttable($copy_attach_from);
750
-				if ($attachmentTable) {
751
-					$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
752
-
753
-					foreach ($attachments as $attach_props) {
754
-						$attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
755
-						$attach_newResourceMsg = mapi_message_createattach($imessage);
756
-						mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
757
-						mapi_savechanges($attach_newResourceMsg);
758
-					}
759
-				}
760
-			}
761
-
762
-			$props = $props + $exception_props;
763
-
764
-			// FIXME: the following piece of code is written to fix the creation
765
-			// of an exception. This is only a quickfix as it is not yet possible
766
-			// to change an existing exception.
767
-			// remove mv properties when needed
768
-			foreach ($props as $propTag => $propVal) {
769
-				if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)) {
770
-					unset($props[$propTag]);
771
-				}
772
-			}
773
-
774
-			mapi_setprops($imessage, $props);
775
-
776
-			$this->setExceptionRecipients($imessage, $exception_recips, true);
777
-
778
-			mapi_savechanges($imessage);
779
-			mapi_savechanges($attachment);
780
-		}
781
-
782
-		/**
783
-		 * Function which deletes the attachment of an exception.
784
-		 *
785
-		 * @param date $base_date base date of the attachment. Should be in GMT. The attachment
786
-		 *                        actually saves the real time of the original date, so we have
787
-		 *                        to check whether it's on the same day.
788
-		 */
789
-		public function deleteExceptionAttachment($base_date) {
790
-			$attachments = mapi_message_getattachmenttable($this->message);
791
-			$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM]);
792
-
793
-			foreach ($attachTable as $attachRow) {
794
-				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
795
-				$exception = mapi_attach_openobj($tempattach);
796
-
797
-				$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
798
-
799
-				if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
800
-					mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
801
-				}
802
-			}
803
-		}
804
-
805
-		/**
806
-		 * Function which deletes all attachments of a message.
807
-		 */
808
-		public function deleteAttachments() {
809
-			$attachments = mapi_message_getattachmenttable($this->message);
810
-			$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
811
-
812
-			foreach ($attachTable as $attachRow) {
813
-				if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
814
-					mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
815
-				}
816
-			}
817
-		}
818
-
819
-		/**
820
-		 * Get an exception attachment based on its basedate.
821
-		 *
822
-		 * @param mixed $base_date
823
-		 */
824
-		public function getExceptionAttachment($base_date) {
825
-			// Retrieve only exceptions which are stored as embedded messages
826
-			$attach_res = [
827
-				RES_AND,
828
-				[
829
-					[
830
-						RES_PROPERTY,
831
-						[
832
-							RELOP => RELOP_EQ,
833
-							ULPROPTAG => PR_ATTACH_METHOD,
834
-							VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
835
-						],
836
-					],
837
-				],
838
-			];
839
-			$attachments = mapi_message_getattachmenttable($this->message);
840
-			$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
841
-
842
-			if (is_array($attachRows)) {
843
-				foreach ($attachRows as $attachRow) {
844
-					$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
845
-					$exception = mapi_attach_openobj($tempattach);
846
-
847
-					$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
848
-
849
-					if (!isset($data[$this->proptags["basedate"]])) {
850
-						// if no basedate found then it could be embedded message so ignore it
851
-						// we need proper restriction to exclude embedded messages as well
852
-						continue;
853
-					}
854
-
855
-					if ($this->isSameDay($this->fromGMT($this->tz, $data[$this->proptags["basedate"]]), $base_date)) {
856
-						return $tempattach;
857
-					}
858
-				}
859
-			}
860
-
861
-			return false;
862
-		}
863
-
864
-		/**
865
-		 * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met:
866
-		 * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end>
867
-		 * - The occurrence isn't specified as a deleted occurrence.
868
-		 *
869
-		 * @param array $items        reference to the array to be added to
870
-		 * @param date  $start        start of timeframe in GMT TIME
871
-		 * @param date  $end          end of timeframe in GMT TIME
872
-		 * @param date  $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
873
-		 * @param int   $startocc     start of occurrence since beginning of day in minutes
874
-		 * @param int   $endocc       end of occurrence since beginning of day in minutes
875
-		 * @param int   $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
876
-		 * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
877
-		 */
878
-		public function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly) {
879
-			$exception = $this->isException($basedate);
880
-			if ($exception) {
881
-				return false;
882
-			}
883
-			$occstart = $basedate + $startocc * 60;
884
-			$occend = $basedate + $endocc * 60;
885
-
886
-			// Convert to GMT
887
-			$occstart = $this->toGMT($tz, $occstart);
888
-			$occend = $this->toGMT($tz, $occend);
889
-
890
-			/**
891
-			 * FIRST PART : Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
892
-			 * see any part of the appointment. Partial overlaps DO match.
893
-			 *
894
-			 * SECOND PART : check if occurrence is not a zero duration occurrence which
895
-			 * starts at 00:00 and ends on 00:00. if it is so, then process
896
-			 * the occurrence and send it in response.
897
-			 */
898
-			if (($occstart >= $end || $occend <= $start) && !($occstart == $occend && $occstart == $start)) {
899
-				return;
900
-			}
901
-
902
-			// Properties for this occurrence are the same as the main object,
903
-			// With these properties overridden
904
-			$newitem = $this->messageprops;
905
-			$newitem[$this->proptags["startdate"]] = $occstart;
906
-			$newitem[$this->proptags["duedate"]] = $occend;
907
-			$newitem[$this->proptags["commonstart"]] = $occstart;
908
-			$newitem[$this->proptags["commonend"]] = $occend;
909
-			$newitem["basedate"] = $basedate;
910
-
911
-			// If reminderonly is set, only add reminders
912
-			if ($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] == false)) {
913
-				return;
914
-			}
915
-
916
-			$items[] = $newitem;
917
-		}
918
-
919
-		/**
920
-		 * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe.
921
-		 *
922
-		 * @param array $items reference to the array to be added to
923
-		 * @param date  $start start of timeframe in GMT TIME
924
-		 * @param date  $end   end of timeframe in GMT TIME
925
-		 */
926
-		public function processExceptionItems(&$items, $start, $end) {
927
-			$limit = 0;
928
-			foreach ($this->recur["changed_occurrences"] as $exception) {
929
-				// Convert to GMT
930
-				$occstart = $this->toGMT($this->tz, $exception["start"]);
931
-				$occend = $this->toGMT($this->tz, $exception["end"]);
932
-
933
-				// Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
934
-				// see any part of the appointment. Partial overlaps DO match.
935
-				if ($occstart >= $end || $occend <= $start) {
936
-					continue;
937
-				}
938
-
939
-				array_push($items, $this->getExceptionProperties($exception));
940
-				if ((count($items) == $limit)) {
941
-					break;
942
-				}
943
-			}
944
-		}
945
-
946
-		/**
947
-		 * Function which verifies if on the given date an exception, delete or change, occurs.
948
-		 *
949
-		 * @param date  $date     the date
950
-		 * @param mixed $basedate
951
-		 *
952
-		 * @return array the exception, true - if an occurrence is deleted on the given date, false - no exception occurs on the given date
953
-		 */
954
-		public function isException($basedate) {
955
-			if ($this->isDeleteException($basedate)) {
956
-				return true;
957
-			}
958
-
959
-			if ($this->getChangeException($basedate) != false) {
960
-				return true;
961
-			}
962
-
963
-			return false;
964
-		}
965
-
966
-		/**
967
-		 * Returns TRUE if there is a DELETE exception on the given base date.
968
-		 *
969
-		 * @param mixed $basedate
970
-		 */
971
-		public function isDeleteException($basedate) {
972
-			// Check if the occurrence is deleted on the specified date
973
-			foreach ($this->recur["deleted_occurrences"] as $deleted) {
974
-				if ($this->isSameDay($deleted, $basedate)) {
975
-					return true;
976
-				}
977
-			}
978
-
979
-			return false;
980
-		}
981
-
982
-		/**
983
-		 * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise.
984
-		 *
985
-		 * @param mixed $basedate
986
-		 */
987
-		public function getChangeException($basedate) {
988
-			// Check if the occurrence is modified on the specified date
989
-			foreach ($this->recur["changed_occurrences"] as $changed) {
990
-				if ($this->isSameDay($changed["basedate"], $basedate)) {
991
-					return $changed;
992
-				}
993
-			}
994
-
995
-			return false;
996
-		}
997
-
998
-		/**
999
-		 * Function to see if two dates are on the same day.
1000
-		 *
1001
-		 * @param date  $time1 date 1
1002
-		 * @param date  $time2 date 2
1003
-		 * @param mixed $date1
1004
-		 * @param mixed $date2
1005
-		 *
1006
-		 * @return bool Returns TRUE when both dates are on the same day
1007
-		 */
1008
-		public function isSameDay($date1, $date2) {
1009
-			$time1 = $this->gmtime($date1);
1010
-			$time2 = $this->gmtime($date2);
1011
-
1012
-			return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"];
1013
-		}
1014
-
1015
-		/**
1016
-		 * Function to get all properties of a single changed exception.
1017
-		 *
1018
-		 * @param date  $date      base date of exception
1019
-		 * @param mixed $exception
1020
-		 *
1021
-		 * @return array associative array of properties for the exception, compatible with
1022
-		 */
1023
-		public function getExceptionProperties($exception) {
1024
-			// Exception has same properties as main object, with some properties overridden:
1025
-			$item = $this->messageprops;
1026
-
1027
-			// Special properties
1028
-			$item["exception"] = true;
1029
-			$item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time !
1030
-
1031
-			// MAPI-compatible properties (you can handle an exception as a normal calendar item like this)
1032
-			$item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]);
1033
-			$item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]);
1034
-			$item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]];
1035
-			$item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]];
1036
-
1037
-			if (isset($exception["subject"])) {
1038
-				$item[$this->proptags["subject"]] = $exception["subject"];
1039
-			}
1040
-
1041
-			if (isset($exception["label"])) {
1042
-				$item[$this->proptags["label"]] = $exception["label"];
1043
-			}
1044
-
1045
-			if (isset($exception["alldayevent"])) {
1046
-				$item[$this->proptags["alldayevent"]] = $exception["alldayevent"];
1047
-			}
1048
-
1049
-			if (isset($exception["location"])) {
1050
-				$item[$this->proptags["location"]] = $exception["location"];
1051
-			}
1052
-
1053
-			if (isset($exception["remind_before"])) {
1054
-				$item[$this->proptags["reminder_minutes"]] = $exception["remind_before"];
1055
-			}
1056
-
1057
-			if (isset($exception["reminder_set"])) {
1058
-				$item[$this->proptags["reminder"]] = $exception["reminder_set"];
1059
-			}
1060
-
1061
-			if (isset($exception["busystatus"])) {
1062
-				$item[$this->proptags["busystatus"]] = $exception["busystatus"];
1063
-			}
1064
-
1065
-			return $item;
1066
-		}
1067
-
1068
-		/**
1069
-		 * Function which sets recipients for an exception.
1070
-		 *
1071
-		 * The $exception_recips can be provided in 2 ways:
1072
-		 *  - A delta which indicates which recipients must be added, removed or deleted.
1073
-		 *  - A complete array of the recipients which should be applied to the message.
1074
-		 *
1075
-		 * The first option is preferred as it will require less work to be executed.
1076
-		 *
1077
-		 * @param resource $message          exception attachment of recurring item
1078
-		 * @param array    $exception_recips list of recipients
1079
-		 * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1080
-		 *                                   message to the attachment by default. False if only the $exception_recips changes should
1081
-		 *                                   be applied.
1082
-		 */
1083
-		public function setExceptionRecipients($message, $exception_recips, $copy_orig_recips = true) {
1084
-			if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) {
1085
-				$this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips);
1086
-			}
1087
-			else {
1088
-				$this->setAllExceptionRecipients($message, $exception_recips);
1089
-			}
1090
-		}
1091
-
1092
-		/**
1093
-		 * Function which applies the provided delta for recipients changes to the exception.
1094
-		 *
1095
-		 * The $exception_recips should be an array containing the following keys:
1096
-		 *  - "add": this contains an array of recipients which must be added
1097
-		 *  - "remove": This contains an array of recipients which must be removed
1098
-		 *  - "modify": This contains an array of recipients which must be modified
1099
-		 *
1100
-		 * @param resource $message          exception attachment of recurring item
1101
-		 * @param array    $exception_recips list of recipients
1102
-		 * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1103
-		 *                                   message to the attachment by default. False if only the $exception_recips changes should
1104
-		 *                                   be applied.
1105
-		 * @param mixed    $exception
1106
-		 */
1107
-		public function setDeltaExceptionRecipients($exception, $exception_recips, $copy_orig_recips) {
1108
-			// Check if the recipients from the original message should be copied,
1109
-			// if so, open the recipient table of the parent message and apply all
1110
-			// rows on the target recipient.
1111
-			if ($copy_orig_recips === true) {
1112
-				$origTable = mapi_message_getrecipienttable($this->message);
1113
-				$recipientRows = mapi_table_queryallrows($origTable, $this->recipprops);
1114
-				mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
1115
-			}
1116
-
1117
-			// Add organizer to meeting only if it is not organized.
1118
-			$msgprops = mapi_getprops($exception, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']]);
1119
-			if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1120
-				$this->addOrganizer($msgprops, $exception_recips['add']);
1121
-			}
1122
-
1123
-			// Remove all deleted recipients
1124
-			if (isset($exception_recips['remove'])) {
1125
-				foreach ($exception_recips['remove'] as &$recip) {
1126
-					if (!isset($recip[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1127
-						$recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1128
-					}
1129
-					else {
1130
-						$recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1131
-					}
1132
-					$recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone;        // No Response required
1133
-				}
1134
-				unset($recip);
1135
-				mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']);
1136
-			}
1137
-
1138
-			// Add all new recipients
1139
-			if (isset($exception_recips['add'])) {
1140
-				mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']);
1141
-			}
1142
-
1143
-			// Modify the existing recipients
1144
-			if (isset($exception_recips['modify'])) {
1145
-				mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']);
1146
-			}
1147
-		}
1148
-
1149
-		/**
1150
-		 * Function which applies the provided recipients to the exception, also checks for deleted recipients.
1151
-		 *
1152
-		 * The $exception_recips should be an array containing all recipients which must be applied
1153
-		 * to the exception. This will copy all recipients from the original message and then start filter
1154
-		 * out all recipients which are not provided by the $exception_recips list.
1155
-		 *
1156
-		 * @param resource $message          exception attachment of recurring item
1157
-		 * @param array    $exception_recips list of recipients
1158
-		 */
1159
-		public function setAllExceptionRecipients($message, $exception_recips) {
1160
-			$deletedRecipients = [];
1161
-			$useMessageRecipients = false;
1162
-
1163
-			$recipientTable = mapi_message_getrecipienttable($message);
1164
-			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1165
-
1166
-			if (empty($recipientRows)) {
1167
-				$useMessageRecipients = true;
1168
-				$recipientTable = mapi_message_getrecipienttable($this->message);
1169
-				$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1170
-			}
1171
-
1172
-			// Add organizer to meeting only if it is not organized.
1173
-			$msgprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']]);
1174
-			if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1175
-				$this->addOrganizer($msgprops, $exception_recips);
1176
-			}
1177
-
1178
-			if (!empty($exception_recips)) {
1179
-				foreach ($recipientRows as $key => $recipient) {
1180
-					$found = false;
1181
-					foreach ($exception_recips as $excep_recip) {
1182
-						if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1183
-							$found = true;
1184
-						}
1185
-					}
1186
-
1187
-					if (!$found) {
1188
-						$foundInDeletedRecipients = false;
1189
-						// Look if the $recipient is in the list of deleted recipients
1190
-						if (!empty($deletedRecipients)) {
1191
-							foreach ($deletedRecipients as $recip) {
1192
-								if ($recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1193
-									$foundInDeletedRecipients = true;
1194
-
1195
-									break;
1196
-								}
1197
-							}
1198
-						}
1199
-
1200
-						// If recipient is not in list of deleted recipient, add him
1201
-						if (!$foundInDeletedRecipients) {
1202
-							if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1203
-								$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1204
-							}
1205
-							else {
1206
-								$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1207
-							}
1208
-							$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;    // No Response required
1209
-							$deletedRecipients[] = $recipient;
1210
-						}
1211
-					}
1212
-
1213
-					// When $message contains a non-empty recipienttable, we must delete the recipients
1214
-					// before re-adding them. However, when $message is doesn't contain any recipients,
1215
-					// we are using the recipient table of the original message ($this->message)
1216
-					// rather then $message. In that case, we don't need to remove the recipients
1217
-					// from the $message, as the recipient table is already empty, and
1218
-					// mapi_message_modifyrecipients() will throw an error.
1219
-					if ($useMessageRecipients === false) {
1220
-						mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
1221
-					}
1222
-				}
1223
-				$exception_recips = array_merge($exception_recips, $deletedRecipients);
1224
-			}
1225
-			else {
1226
-				$exception_recips = $recipientRows;
1227
-			}
1228
-
1229
-			if (!empty($exception_recips)) {
1230
-				// Set the new list of recipients on the exception message, this also removes the existing recipients
1231
-				mapi_message_modifyrecipients($message, 0, $exception_recips);
1232
-			}
1233
-		}
1234
-
1235
-		/**
1236
-		 * Function returns basedates of all changed occurrences.
1237
-		 *
1238
-		 *@return array array(
1239
-		 * 0 => 123459321
1240
-		 * )
1241
-		 */
1242
-		public function getAllExceptions() {
1243
-			$result = false;
1244
-			if (!empty($this->recur["changed_occurrences"])) {
1245
-				$result = [];
1246
-				foreach ($this->recur["changed_occurrences"] as $exception) {
1247
-					$result[] = $exception["basedate"];
1248
-				}
1249
-
1250
-				return $result;
1251
-			}
1252
-
1253
-			return $result;
1254
-		}
1255
-
1256
-		/**
1257
-		 *  Function which adds organizer to recipient list which is passed.
1258
-		 *  This function also checks if it has organizer.
1259
-		 *
1260
-		 * @param array $messageProps message properties
1261
-		 * @param array $recipients   recipients list of message
1262
-		 * @param bool  $isException  true if we are processing recipient of exception
1263
-		 */
1264
-		public function addOrganizer($messageProps, &$recipients, $isException = false) {
1265
-			$hasOrganizer = false;
1266
-			// Check if meeting already has an organizer.
1267
-			foreach ($recipients as $key => $recipient) {
1268
-				if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
1269
-					$hasOrganizer = true;
1270
-				}
1271
-				elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
1272
-					// Recipients for an occurrence
1273
-					$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
1274
-				}
1275
-			}
1276
-
1277
-			if (!$hasOrganizer) {
1278
-				// Create organizer.
1279
-				$organizer = [];
1280
-				$organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
1281
-				$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1282
-				$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1283
-				$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
1284
-				$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1285
-				$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
1286
-				$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
1287
-				$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
1288
-				$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
1289
-
1290
-				// Add organizer to recipients list.
1291
-				array_unshift($recipients, $organizer);
1292
-			}
1293
-		}
1294
-	}
1295
-
1296
-	/*
698
+        public function deleteException($base_date) {
699
+            // Remove all exceptions on $base_date from the deleted and changed occurrences lists
700
+
701
+            // Remove all items in $todelete from deleted_occurrences
702
+            $new = [];
703
+
704
+            foreach ($this->recur["deleted_occurrences"] as $entry) {
705
+                if ($entry != $base_date) {
706
+                    $new[] = $entry;
707
+                }
708
+            }
709
+            $this->recur["deleted_occurrences"] = $new;
710
+
711
+            $new = [];
712
+
713
+            foreach ($this->recur["changed_occurrences"] as $entry) {
714
+                if (!$this->isSameDay($entry["basedate"], $base_date)) {
715
+                    $new[] = $entry;
716
+                }
717
+                else {
718
+                    $this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
719
+                }
720
+            }
721
+
722
+            $this->recur["changed_occurrences"] = $new;
723
+        }
724
+
725
+        /**
726
+         * Function which saves the exception data in an attachment.
727
+         *
728
+         * @param array        $exception_props  the exception data (like any other MAPI appointment)
729
+         * @param array        $exception_recips list of recipients
730
+         * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
731
+         */
732
+        public function createExceptionAttachment($exception_props, $exception_recips = [], $copy_attach_from = false) {
733
+            // Create new attachment.
734
+            $attachment = mapi_message_createattach($this->message);
735
+            $props = [];
736
+            $props[PR_ATTACHMENT_FLAGS] = 2;
737
+            $props[PR_ATTACHMENT_HIDDEN] = true;
738
+            $props[PR_ATTACHMENT_LINKID] = 0;
739
+            $props[PR_ATTACH_FLAGS] = 0;
740
+            $props[PR_ATTACH_METHOD] = 5;
741
+            $props[PR_DISPLAY_NAME] = "Exception";
742
+            $props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
743
+            $props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
744
+            mapi_setprops($attachment, $props);
745
+
746
+            $imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
747
+
748
+            if ($copy_attach_from) {
749
+                $attachmentTable = mapi_message_getattachmenttable($copy_attach_from);
750
+                if ($attachmentTable) {
751
+                    $attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
752
+
753
+                    foreach ($attachments as $attach_props) {
754
+                        $attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
755
+                        $attach_newResourceMsg = mapi_message_createattach($imessage);
756
+                        mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
757
+                        mapi_savechanges($attach_newResourceMsg);
758
+                    }
759
+                }
760
+            }
761
+
762
+            $props = $props + $exception_props;
763
+
764
+            // FIXME: the following piece of code is written to fix the creation
765
+            // of an exception. This is only a quickfix as it is not yet possible
766
+            // to change an existing exception.
767
+            // remove mv properties when needed
768
+            foreach ($props as $propTag => $propVal) {
769
+                if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)) {
770
+                    unset($props[$propTag]);
771
+                }
772
+            }
773
+
774
+            mapi_setprops($imessage, $props);
775
+
776
+            $this->setExceptionRecipients($imessage, $exception_recips, true);
777
+
778
+            mapi_savechanges($imessage);
779
+            mapi_savechanges($attachment);
780
+        }
781
+
782
+        /**
783
+         * Function which deletes the attachment of an exception.
784
+         *
785
+         * @param date $base_date base date of the attachment. Should be in GMT. The attachment
786
+         *                        actually saves the real time of the original date, so we have
787
+         *                        to check whether it's on the same day.
788
+         */
789
+        public function deleteExceptionAttachment($base_date) {
790
+            $attachments = mapi_message_getattachmenttable($this->message);
791
+            $attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM]);
792
+
793
+            foreach ($attachTable as $attachRow) {
794
+                $tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
795
+                $exception = mapi_attach_openobj($tempattach);
796
+
797
+                $data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
798
+
799
+                if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
800
+                    mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
801
+                }
802
+            }
803
+        }
804
+
805
+        /**
806
+         * Function which deletes all attachments of a message.
807
+         */
808
+        public function deleteAttachments() {
809
+            $attachments = mapi_message_getattachmenttable($this->message);
810
+            $attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
811
+
812
+            foreach ($attachTable as $attachRow) {
813
+                if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
814
+                    mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
815
+                }
816
+            }
817
+        }
818
+
819
+        /**
820
+         * Get an exception attachment based on its basedate.
821
+         *
822
+         * @param mixed $base_date
823
+         */
824
+        public function getExceptionAttachment($base_date) {
825
+            // Retrieve only exceptions which are stored as embedded messages
826
+            $attach_res = [
827
+                RES_AND,
828
+                [
829
+                    [
830
+                        RES_PROPERTY,
831
+                        [
832
+                            RELOP => RELOP_EQ,
833
+                            ULPROPTAG => PR_ATTACH_METHOD,
834
+                            VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
835
+                        ],
836
+                    ],
837
+                ],
838
+            ];
839
+            $attachments = mapi_message_getattachmenttable($this->message);
840
+            $attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
841
+
842
+            if (is_array($attachRows)) {
843
+                foreach ($attachRows as $attachRow) {
844
+                    $tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
845
+                    $exception = mapi_attach_openobj($tempattach);
846
+
847
+                    $data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
848
+
849
+                    if (!isset($data[$this->proptags["basedate"]])) {
850
+                        // if no basedate found then it could be embedded message so ignore it
851
+                        // we need proper restriction to exclude embedded messages as well
852
+                        continue;
853
+                    }
854
+
855
+                    if ($this->isSameDay($this->fromGMT($this->tz, $data[$this->proptags["basedate"]]), $base_date)) {
856
+                        return $tempattach;
857
+                    }
858
+                }
859
+            }
860
+
861
+            return false;
862
+        }
863
+
864
+        /**
865
+         * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met:
866
+         * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end>
867
+         * - The occurrence isn't specified as a deleted occurrence.
868
+         *
869
+         * @param array $items        reference to the array to be added to
870
+         * @param date  $start        start of timeframe in GMT TIME
871
+         * @param date  $end          end of timeframe in GMT TIME
872
+         * @param date  $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
873
+         * @param int   $startocc     start of occurrence since beginning of day in minutes
874
+         * @param int   $endocc       end of occurrence since beginning of day in minutes
875
+         * @param int   $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
876
+         * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
877
+         */
878
+        public function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly) {
879
+            $exception = $this->isException($basedate);
880
+            if ($exception) {
881
+                return false;
882
+            }
883
+            $occstart = $basedate + $startocc * 60;
884
+            $occend = $basedate + $endocc * 60;
885
+
886
+            // Convert to GMT
887
+            $occstart = $this->toGMT($tz, $occstart);
888
+            $occend = $this->toGMT($tz, $occend);
889
+
890
+            /**
891
+             * FIRST PART : Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
892
+             * see any part of the appointment. Partial overlaps DO match.
893
+             *
894
+             * SECOND PART : check if occurrence is not a zero duration occurrence which
895
+             * starts at 00:00 and ends on 00:00. if it is so, then process
896
+             * the occurrence and send it in response.
897
+             */
898
+            if (($occstart >= $end || $occend <= $start) && !($occstart == $occend && $occstart == $start)) {
899
+                return;
900
+            }
901
+
902
+            // Properties for this occurrence are the same as the main object,
903
+            // With these properties overridden
904
+            $newitem = $this->messageprops;
905
+            $newitem[$this->proptags["startdate"]] = $occstart;
906
+            $newitem[$this->proptags["duedate"]] = $occend;
907
+            $newitem[$this->proptags["commonstart"]] = $occstart;
908
+            $newitem[$this->proptags["commonend"]] = $occend;
909
+            $newitem["basedate"] = $basedate;
910
+
911
+            // If reminderonly is set, only add reminders
912
+            if ($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] == false)) {
913
+                return;
914
+            }
915
+
916
+            $items[] = $newitem;
917
+        }
918
+
919
+        /**
920
+         * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe.
921
+         *
922
+         * @param array $items reference to the array to be added to
923
+         * @param date  $start start of timeframe in GMT TIME
924
+         * @param date  $end   end of timeframe in GMT TIME
925
+         */
926
+        public function processExceptionItems(&$items, $start, $end) {
927
+            $limit = 0;
928
+            foreach ($this->recur["changed_occurrences"] as $exception) {
929
+                // Convert to GMT
930
+                $occstart = $this->toGMT($this->tz, $exception["start"]);
931
+                $occend = $this->toGMT($this->tz, $exception["end"]);
932
+
933
+                // Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
934
+                // see any part of the appointment. Partial overlaps DO match.
935
+                if ($occstart >= $end || $occend <= $start) {
936
+                    continue;
937
+                }
938
+
939
+                array_push($items, $this->getExceptionProperties($exception));
940
+                if ((count($items) == $limit)) {
941
+                    break;
942
+                }
943
+            }
944
+        }
945
+
946
+        /**
947
+         * Function which verifies if on the given date an exception, delete or change, occurs.
948
+         *
949
+         * @param date  $date     the date
950
+         * @param mixed $basedate
951
+         *
952
+         * @return array the exception, true - if an occurrence is deleted on the given date, false - no exception occurs on the given date
953
+         */
954
+        public function isException($basedate) {
955
+            if ($this->isDeleteException($basedate)) {
956
+                return true;
957
+            }
958
+
959
+            if ($this->getChangeException($basedate) != false) {
960
+                return true;
961
+            }
962
+
963
+            return false;
964
+        }
965
+
966
+        /**
967
+         * Returns TRUE if there is a DELETE exception on the given base date.
968
+         *
969
+         * @param mixed $basedate
970
+         */
971
+        public function isDeleteException($basedate) {
972
+            // Check if the occurrence is deleted on the specified date
973
+            foreach ($this->recur["deleted_occurrences"] as $deleted) {
974
+                if ($this->isSameDay($deleted, $basedate)) {
975
+                    return true;
976
+                }
977
+            }
978
+
979
+            return false;
980
+        }
981
+
982
+        /**
983
+         * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise.
984
+         *
985
+         * @param mixed $basedate
986
+         */
987
+        public function getChangeException($basedate) {
988
+            // Check if the occurrence is modified on the specified date
989
+            foreach ($this->recur["changed_occurrences"] as $changed) {
990
+                if ($this->isSameDay($changed["basedate"], $basedate)) {
991
+                    return $changed;
992
+                }
993
+            }
994
+
995
+            return false;
996
+        }
997
+
998
+        /**
999
+         * Function to see if two dates are on the same day.
1000
+         *
1001
+         * @param date  $time1 date 1
1002
+         * @param date  $time2 date 2
1003
+         * @param mixed $date1
1004
+         * @param mixed $date2
1005
+         *
1006
+         * @return bool Returns TRUE when both dates are on the same day
1007
+         */
1008
+        public function isSameDay($date1, $date2) {
1009
+            $time1 = $this->gmtime($date1);
1010
+            $time2 = $this->gmtime($date2);
1011
+
1012
+            return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"];
1013
+        }
1014
+
1015
+        /**
1016
+         * Function to get all properties of a single changed exception.
1017
+         *
1018
+         * @param date  $date      base date of exception
1019
+         * @param mixed $exception
1020
+         *
1021
+         * @return array associative array of properties for the exception, compatible with
1022
+         */
1023
+        public function getExceptionProperties($exception) {
1024
+            // Exception has same properties as main object, with some properties overridden:
1025
+            $item = $this->messageprops;
1026
+
1027
+            // Special properties
1028
+            $item["exception"] = true;
1029
+            $item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time !
1030
+
1031
+            // MAPI-compatible properties (you can handle an exception as a normal calendar item like this)
1032
+            $item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]);
1033
+            $item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]);
1034
+            $item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]];
1035
+            $item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]];
1036
+
1037
+            if (isset($exception["subject"])) {
1038
+                $item[$this->proptags["subject"]] = $exception["subject"];
1039
+            }
1040
+
1041
+            if (isset($exception["label"])) {
1042
+                $item[$this->proptags["label"]] = $exception["label"];
1043
+            }
1044
+
1045
+            if (isset($exception["alldayevent"])) {
1046
+                $item[$this->proptags["alldayevent"]] = $exception["alldayevent"];
1047
+            }
1048
+
1049
+            if (isset($exception["location"])) {
1050
+                $item[$this->proptags["location"]] = $exception["location"];
1051
+            }
1052
+
1053
+            if (isset($exception["remind_before"])) {
1054
+                $item[$this->proptags["reminder_minutes"]] = $exception["remind_before"];
1055
+            }
1056
+
1057
+            if (isset($exception["reminder_set"])) {
1058
+                $item[$this->proptags["reminder"]] = $exception["reminder_set"];
1059
+            }
1060
+
1061
+            if (isset($exception["busystatus"])) {
1062
+                $item[$this->proptags["busystatus"]] = $exception["busystatus"];
1063
+            }
1064
+
1065
+            return $item;
1066
+        }
1067
+
1068
+        /**
1069
+         * Function which sets recipients for an exception.
1070
+         *
1071
+         * The $exception_recips can be provided in 2 ways:
1072
+         *  - A delta which indicates which recipients must be added, removed or deleted.
1073
+         *  - A complete array of the recipients which should be applied to the message.
1074
+         *
1075
+         * The first option is preferred as it will require less work to be executed.
1076
+         *
1077
+         * @param resource $message          exception attachment of recurring item
1078
+         * @param array    $exception_recips list of recipients
1079
+         * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1080
+         *                                   message to the attachment by default. False if only the $exception_recips changes should
1081
+         *                                   be applied.
1082
+         */
1083
+        public function setExceptionRecipients($message, $exception_recips, $copy_orig_recips = true) {
1084
+            if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) {
1085
+                $this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips);
1086
+            }
1087
+            else {
1088
+                $this->setAllExceptionRecipients($message, $exception_recips);
1089
+            }
1090
+        }
1091
+
1092
+        /**
1093
+         * Function which applies the provided delta for recipients changes to the exception.
1094
+         *
1095
+         * The $exception_recips should be an array containing the following keys:
1096
+         *  - "add": this contains an array of recipients which must be added
1097
+         *  - "remove": This contains an array of recipients which must be removed
1098
+         *  - "modify": This contains an array of recipients which must be modified
1099
+         *
1100
+         * @param resource $message          exception attachment of recurring item
1101
+         * @param array    $exception_recips list of recipients
1102
+         * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1103
+         *                                   message to the attachment by default. False if only the $exception_recips changes should
1104
+         *                                   be applied.
1105
+         * @param mixed    $exception
1106
+         */
1107
+        public function setDeltaExceptionRecipients($exception, $exception_recips, $copy_orig_recips) {
1108
+            // Check if the recipients from the original message should be copied,
1109
+            // if so, open the recipient table of the parent message and apply all
1110
+            // rows on the target recipient.
1111
+            if ($copy_orig_recips === true) {
1112
+                $origTable = mapi_message_getrecipienttable($this->message);
1113
+                $recipientRows = mapi_table_queryallrows($origTable, $this->recipprops);
1114
+                mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
1115
+            }
1116
+
1117
+            // Add organizer to meeting only if it is not organized.
1118
+            $msgprops = mapi_getprops($exception, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']]);
1119
+            if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1120
+                $this->addOrganizer($msgprops, $exception_recips['add']);
1121
+            }
1122
+
1123
+            // Remove all deleted recipients
1124
+            if (isset($exception_recips['remove'])) {
1125
+                foreach ($exception_recips['remove'] as &$recip) {
1126
+                    if (!isset($recip[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1127
+                        $recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1128
+                    }
1129
+                    else {
1130
+                        $recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1131
+                    }
1132
+                    $recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone;        // No Response required
1133
+                }
1134
+                unset($recip);
1135
+                mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']);
1136
+            }
1137
+
1138
+            // Add all new recipients
1139
+            if (isset($exception_recips['add'])) {
1140
+                mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']);
1141
+            }
1142
+
1143
+            // Modify the existing recipients
1144
+            if (isset($exception_recips['modify'])) {
1145
+                mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']);
1146
+            }
1147
+        }
1148
+
1149
+        /**
1150
+         * Function which applies the provided recipients to the exception, also checks for deleted recipients.
1151
+         *
1152
+         * The $exception_recips should be an array containing all recipients which must be applied
1153
+         * to the exception. This will copy all recipients from the original message and then start filter
1154
+         * out all recipients which are not provided by the $exception_recips list.
1155
+         *
1156
+         * @param resource $message          exception attachment of recurring item
1157
+         * @param array    $exception_recips list of recipients
1158
+         */
1159
+        public function setAllExceptionRecipients($message, $exception_recips) {
1160
+            $deletedRecipients = [];
1161
+            $useMessageRecipients = false;
1162
+
1163
+            $recipientTable = mapi_message_getrecipienttable($message);
1164
+            $recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1165
+
1166
+            if (empty($recipientRows)) {
1167
+                $useMessageRecipients = true;
1168
+                $recipientTable = mapi_message_getrecipienttable($this->message);
1169
+                $recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1170
+            }
1171
+
1172
+            // Add organizer to meeting only if it is not organized.
1173
+            $msgprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']]);
1174
+            if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1175
+                $this->addOrganizer($msgprops, $exception_recips);
1176
+            }
1177
+
1178
+            if (!empty($exception_recips)) {
1179
+                foreach ($recipientRows as $key => $recipient) {
1180
+                    $found = false;
1181
+                    foreach ($exception_recips as $excep_recip) {
1182
+                        if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1183
+                            $found = true;
1184
+                        }
1185
+                    }
1186
+
1187
+                    if (!$found) {
1188
+                        $foundInDeletedRecipients = false;
1189
+                        // Look if the $recipient is in the list of deleted recipients
1190
+                        if (!empty($deletedRecipients)) {
1191
+                            foreach ($deletedRecipients as $recip) {
1192
+                                if ($recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1193
+                                    $foundInDeletedRecipients = true;
1194
+
1195
+                                    break;
1196
+                                }
1197
+                            }
1198
+                        }
1199
+
1200
+                        // If recipient is not in list of deleted recipient, add him
1201
+                        if (!$foundInDeletedRecipients) {
1202
+                            if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1203
+                                $recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1204
+                            }
1205
+                            else {
1206
+                                $recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1207
+                            }
1208
+                            $recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;    // No Response required
1209
+                            $deletedRecipients[] = $recipient;
1210
+                        }
1211
+                    }
1212
+
1213
+                    // When $message contains a non-empty recipienttable, we must delete the recipients
1214
+                    // before re-adding them. However, when $message is doesn't contain any recipients,
1215
+                    // we are using the recipient table of the original message ($this->message)
1216
+                    // rather then $message. In that case, we don't need to remove the recipients
1217
+                    // from the $message, as the recipient table is already empty, and
1218
+                    // mapi_message_modifyrecipients() will throw an error.
1219
+                    if ($useMessageRecipients === false) {
1220
+                        mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
1221
+                    }
1222
+                }
1223
+                $exception_recips = array_merge($exception_recips, $deletedRecipients);
1224
+            }
1225
+            else {
1226
+                $exception_recips = $recipientRows;
1227
+            }
1228
+
1229
+            if (!empty($exception_recips)) {
1230
+                // Set the new list of recipients on the exception message, this also removes the existing recipients
1231
+                mapi_message_modifyrecipients($message, 0, $exception_recips);
1232
+            }
1233
+        }
1234
+
1235
+        /**
1236
+         * Function returns basedates of all changed occurrences.
1237
+         *
1238
+         *@return array array(
1239
+         * 0 => 123459321
1240
+         * )
1241
+         */
1242
+        public function getAllExceptions() {
1243
+            $result = false;
1244
+            if (!empty($this->recur["changed_occurrences"])) {
1245
+                $result = [];
1246
+                foreach ($this->recur["changed_occurrences"] as $exception) {
1247
+                    $result[] = $exception["basedate"];
1248
+                }
1249
+
1250
+                return $result;
1251
+            }
1252
+
1253
+            return $result;
1254
+        }
1255
+
1256
+        /**
1257
+         *  Function which adds organizer to recipient list which is passed.
1258
+         *  This function also checks if it has organizer.
1259
+         *
1260
+         * @param array $messageProps message properties
1261
+         * @param array $recipients   recipients list of message
1262
+         * @param bool  $isException  true if we are processing recipient of exception
1263
+         */
1264
+        public function addOrganizer($messageProps, &$recipients, $isException = false) {
1265
+            $hasOrganizer = false;
1266
+            // Check if meeting already has an organizer.
1267
+            foreach ($recipients as $key => $recipient) {
1268
+                if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
1269
+                    $hasOrganizer = true;
1270
+                }
1271
+                elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
1272
+                    // Recipients for an occurrence
1273
+                    $recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
1274
+                }
1275
+            }
1276
+
1277
+            if (!$hasOrganizer) {
1278
+                // Create organizer.
1279
+                $organizer = [];
1280
+                $organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
1281
+                $organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1282
+                $organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1283
+                $organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
1284
+                $organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1285
+                $organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
1286
+                $organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
1287
+                $organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
1288
+                $organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
1289
+
1290
+                // Add organizer to recipients list.
1291
+                array_unshift($recipients, $organizer);
1292
+            }
1293
+        }
1294
+    }
1295
+
1296
+    /*
1297 1297
 
1298 1298
 	From http://www.ohelp-one.com/new-6765483-3268.html:
1299 1299
 
Please login to merge, or discard this patch.
mapi/mapitags.php 1 patch
Indentation   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -6,7 +6,7 @@
 block discarded – undo
6 6
  */
7 7
 
8 8
 if (!function_exists("mapi_prop_tag")) {
9
-	throw new FatalMisconfigurationException("PHP-MAPI extension is not available");
9
+    throw new FatalMisconfigurationException("PHP-MAPI extension is not available");
10 10
 }
11 11
 
12 12
 define('PR_ACKNOWLEDGEMENT_MODE', mapi_prop_tag(PT_LONG, 0x0001));
Please login to merge, or discard this patch.
mapi/class.freebusypublish.php 1 patch
Indentation   +389 added lines, -389 removed lines patch added patch discarded remove patch
@@ -6,393 +6,393 @@
 block discarded – undo
6 6
  */
7 7
 
8 8
 class FreeBusyPublish {
9
-	public $session;
10
-	public $calendar;
11
-	public $entryid;
12
-	public $starttime;
13
-	public $length;
14
-	public $store;
15
-	public $proptags;
16
-
17
-	/**
18
-	 * Constructor.
19
-	 *
20
-	 * @param mapi_session $session  MAPI Session
21
-	 * @param mapi_folder  $calendar Calendar to publish
22
-	 * @param string       $entryid  AddressBook Entry ID for the user we're publishing for
23
-	 * @param mixed        $store
24
-	 */
25
-	public function __construct($session, $store, $calendar, $entryid) {
26
-		$properties["entryid"] = PR_ENTRYID;
27
-		$properties["parent_entryid"] = PR_PARENT_ENTRYID;
28
-		$properties["message_class"] = PR_MESSAGE_CLASS;
29
-		$properties["icon_index"] = PR_ICON_INDEX;
30
-		$properties["subject"] = PR_SUBJECT;
31
-		$properties["display_to"] = PR_DISPLAY_TO;
32
-		$properties["importance"] = PR_IMPORTANCE;
33
-		$properties["sensitivity"] = PR_SENSITIVITY;
34
-		$properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d";
35
-		$properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e";
36
-		$properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223";
37
-		$properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:0x8216";
38
-		$properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205";
39
-		$properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
40
-		$properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:0x8215";
41
-		$properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
42
-		$properties["meeting"] = "PT_LONG:PSETID_Appointment:0x8217";
43
-		$properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8235";
44
-		$properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8236";
45
-		$properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208";
46
-		$properties["duration"] = "PT_LONG:PSETID_Appointment:0x8213";
47
-		$properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
48
-		$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
49
-		$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
50
-		$properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
51
-		$properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
52
-		$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
53
-		$properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502";
54
-		$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
55
-		$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
56
-		$properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228";
57
-		$properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233";
58
-		$this->proptags = getPropIdsFromStrings($store, $properties);
59
-
60
-		$this->session = $session;
61
-		$this->calendar = $calendar;
62
-		$this->entryid = $entryid;
63
-		$this->store = $store;
64
-	}
65
-
66
-	/**
67
-	 * Function is used to get the calendar data based on give date range.
68
-	 *
69
-	 * @param timestamp $starttime time from which to get the calendar data
70
-	 * @param timestamp $length    time up till now get the calendar data
71
-	 *
72
-	 * @return array return the calendar data array
73
-	 */
74
-	public function getCalendarData($starttime, $length) {
75
-		$start = $starttime;
76
-		$end = $length;
77
-
78
-		// Get all the items in the calendar that we need
79
-
80
-		$calendaritems = [];
81
-
82
-		$restrict = [
83
-			RES_OR,
84
-			[
85
-				// OR
86
-				// (item[start] >= start && item[start] <= end)
87
-				[
88
-					RES_AND,
89
-					[
90
-						[
91
-							RES_PROPERTY,
92
-							[
93
-								RELOP => RELOP_GE,
94
-								ULPROPTAG => $this->proptags["startdate"],
95
-								VALUE => $start,
96
-							],
97
-						],
98
-						[
99
-							RES_PROPERTY,
100
-							[
101
-								RELOP => RELOP_LE,
102
-								ULPROPTAG => $this->proptags["startdate"],
103
-								VALUE => $end,
104
-							],
105
-						],
106
-					],
107
-				],
108
-				// OR
109
-				// (item[end]   >= start && item[end]   <= end)
110
-				[
111
-					RES_AND,
112
-					[
113
-						[
114
-							RES_PROPERTY,
115
-							[
116
-								RELOP => RELOP_GE,
117
-								ULPROPTAG => $this->proptags["duedate"],
118
-								VALUE => $start,
119
-							],
120
-						],
121
-						[
122
-							RES_PROPERTY,
123
-							[
124
-								RELOP => RELOP_LE,
125
-								ULPROPTAG => $this->proptags["duedate"],
126
-								VALUE => $end,
127
-							],
128
-						],
129
-					],
130
-				],
131
-				// OR
132
-				// (item[start] <  start && item[end]   >  end)
133
-				[
134
-					RES_AND,
135
-					[
136
-						[
137
-							RES_PROPERTY,
138
-							[
139
-								RELOP => RELOP_LT,
140
-								ULPROPTAG => $this->proptags["startdate"],
141
-								VALUE => $start,
142
-							],
143
-						],
144
-						[
145
-							RES_PROPERTY,
146
-							[
147
-								RELOP => RELOP_GT,
148
-								ULPROPTAG => $this->proptags["duedate"],
149
-								VALUE => $end,
150
-							],
151
-						],
152
-					],
153
-				],
154
-				// OR
155
-				[
156
-					RES_OR,
157
-					[
158
-						// OR
159
-						// (EXIST(ecurrence_enddate_property) && item[isRecurring] == true && item[end] >= start)
160
-						[
161
-							RES_AND,
162
-							[
163
-								[
164
-									RES_EXIST,
165
-									[ULPROPTAG => $this->proptags["enddate_recurring"]],
166
-								],
167
-								[
168
-									RES_PROPERTY,
169
-									[
170
-										RELOP => RELOP_EQ,
171
-										ULPROPTAG => $this->proptags["recurring"],
172
-										VALUE => true,
173
-									],
174
-								],
175
-								[
176
-									RES_PROPERTY,
177
-									[
178
-										RELOP => RELOP_GE,
179
-										ULPROPTAG => $this->proptags["enddate_recurring"],
180
-										VALUE => $start,
181
-									],
182
-								],
183
-							],
184
-						],
185
-						// OR
186
-						// (!EXIST(ecurrence_enddate_property) && item[isRecurring] == true && item[start] <= end)
187
-						[
188
-							RES_AND,
189
-							[
190
-								[
191
-									RES_NOT,
192
-									[
193
-										[
194
-											RES_EXIST,
195
-											[ULPROPTAG => $this->proptags["enddate_recurring"],
196
-											],
197
-										],
198
-									],
199
-								],
200
-								[
201
-									RES_PROPERTY,
202
-									[
203
-										RELOP => RELOP_LE,
204
-										ULPROPTAG => $this->proptags["startdate"],
205
-										VALUE => $end,
206
-									],
207
-								],
208
-								[
209
-									RES_PROPERTY,
210
-									[
211
-										RELOP => RELOP_EQ,
212
-										ULPROPTAG => $this->proptags["recurring"],
213
-										VALUE => true,
214
-									],
215
-								],
216
-							],
217
-						],
218
-					],
219
-				], // EXISTS OR
220
-			],
221
-		];        // global OR
222
-
223
-		$contents = mapi_folder_getcontentstable($this->calendar);
224
-		mapi_table_restrict($contents, $restrict);
225
-
226
-		while (1) {
227
-			$rows = mapi_table_queryrows($contents, array_values($this->proptags), 0, 50);
228
-
229
-			if (!is_array($rows)) {
230
-				break;
231
-			}
232
-
233
-			if (empty($rows)) {
234
-				break;
235
-			}
236
-
237
-			foreach ($rows as $row) {
238
-				$occurrences = [];
239
-				if (isset($row[$this->proptags['recurring']]) && $row[$this->proptags['recurring']]) {
240
-					$recur = new Recurrence($this->store, $row);
241
-
242
-					$occurrences = $recur->getItems($starttime, $length);
243
-				}
244
-				else {
245
-					$occurrences[] = $row;
246
-				}
247
-
248
-				$calendaritems = array_merge($calendaritems, $occurrences);
249
-			}
250
-		}
251
-
252
-		// $calendaritems now contains all the calendar items in the specified time
253
-		// frame. We now need to merge these into a flat array of begin/end/status
254
-		// objects. This also filters out all the 'free' items (status 0)
255
-		return $this->mergeItemsFB($calendaritems);
256
-	}
257
-
258
-	/**
259
-	 * Publishes Free/Busy information of user.
260
-	 *
261
-	 * @param timestamp $starttime Time from which to publish data  (usually now)
262
-	 * @param timestamp $length    Time of seconds from $starttime we should publish
263
-	 * @param mixed     $start
264
-	 * @param mixed     $end
265
-	 */
266
-	public function publishFB($start, $end) {
267
-		$freebusy = $this->getCalendarData($start, $end);
268
-
269
-		// Get the FB interface
270
-		try {
271
-			$fbsupport = mapi_freebusysupport_open($this->session, $this->store);
272
-		}
273
-		catch (MAPIException $e) {
274
-			if ($e->getCode() == MAPI_E_NOT_FOUND) {
275
-				$e->setHandled();
276
-				SLog::Write(LOGLEVEL_WARN, "Error in opening freebusysupport object.");
277
-			}
278
-		}
279
-
280
-		// Open updater for this user
281
-		if (isset($fbsupport) && $fbsupport) {
282
-			$updaters = mapi_freebusysupport_loadupdate($fbsupport, [$this->entryid]);
283
-
284
-			$updater = $updaters[0];
285
-
286
-			// Send the data
287
-			mapi_freebusyupdate_reset($updater);
288
-			mapi_freebusyupdate_publish($updater, $freebusy);
289
-			mapi_freebusyupdate_savechanges($updater, $start - 24 * 60 * 60, $end);
290
-
291
-			// We're finished
292
-			mapi_freebusysupport_close($fbsupport);
293
-		}
294
-		else {
295
-			SLog::Write(LOGLEVEL_WARN, "FreeBusyPublish is not available");
296
-		}
297
-	}
298
-
299
-	/**
300
-	 * Sorts by timestamp, if equal, then end before start.
301
-	 *
302
-	 * @param mixed $a
303
-	 * @param mixed $b
304
-	 */
305
-	public function cmp($a, $b) {
306
-		if ($a["time"] == $b["time"]) {
307
-			if ($a["type"] < $b["type"]) {
308
-				return 1;
309
-			}
310
-			if ($a["type"] > $b["type"]) {
311
-				return -1;
312
-			}
313
-
314
-			return 0;
315
-		}
316
-
317
-		return $a["time"] > $b["time"] ? 1 : -1;
318
-	}
319
-
320
-	/**
321
-	 * Function mergeItems.
322
-	 *
323
-	 * @author Steve Hardy
324
-	 *
325
-	 * @param mixed $items
326
-	 */
327
-	public function mergeItemsFB($items) {
328
-		$merged = [];
329
-		$timestamps = [];
330
-		$csubj = [];
331
-		$cbusy = [];
332
-		$level = 0;
333
-		$laststart = null;
334
-
335
-		foreach ($items as $item) {
336
-			$ts["type"] = 0;
337
-			$ts["time"] = $item[$this->proptags["startdate"]];
338
-			$ts["subject"] = $item[PR_SUBJECT];
339
-			$ts["status"] = (isset($item[$this->proptags["busystatus"]])) ? $item[$this->proptags["busystatus"]] : fbFree; // ZP-197
340
-			$timestamps[] = $ts;
341
-
342
-			$ts["type"] = 1;
343
-			$ts["time"] = $item[$this->proptags["duedate"]];
344
-			$ts["subject"] = $item[PR_SUBJECT];
345
-			$ts["status"] = (isset($item[$this->proptags["busystatus"]])) ? $item[$this->proptags["busystatus"]] : fbFree; // ZP-197
346
-			$timestamps[] = $ts;
347
-		}
348
-
349
-		usort($timestamps, [$this, "cmp"]);
350
-		$laststart = 0; // seb added
351
-
352
-		foreach ($timestamps as $ts) {
353
-			switch ($ts["type"]) {
354
-				case 0: // Start
355
-					if ($level != 0 && $laststart != $ts["time"]) {
356
-						$newitem["start"] = $laststart;
357
-						$newitem["end"] = $ts["time"];
358
-						$newitem["subject"] = join(",", $csubj);
359
-						$newitem["status"] = !empty($cbusy) ? max($cbusy) : 0;
360
-						if ($newitem["status"] > 0) {
361
-							$merged[] = $newitem;
362
-						}
363
-					}
364
-
365
-					++$level;
366
-
367
-					$csubj[] = $ts["subject"];
368
-					$cbusy[] = $ts["status"];
369
-
370
-					$laststart = $ts["time"];
371
-
372
-					break;
373
-
374
-				case 1: // End
375
-					if ($laststart != $ts["time"]) {
376
-						$newitem["start"] = $laststart;
377
-						$newitem["end"] = $ts["time"];
378
-						$newitem["subject"] = join(",", $csubj);
379
-						$newitem["status"] = !empty($cbusy) ? max($cbusy) : 0;
380
-						if ($newitem["status"] > 0) {
381
-							$merged[] = $newitem;
382
-						}
383
-					}
384
-
385
-					--$level;
386
-
387
-					array_splice($csubj, array_search($ts["subject"], $csubj, 1), 1);
388
-					array_splice($cbusy, array_search($ts["status"], $cbusy, 1), 1);
389
-
390
-					$laststart = $ts["time"];
391
-
392
-					break;
393
-			}
394
-		}
395
-
396
-		return $merged;
397
-	}
9
+    public $session;
10
+    public $calendar;
11
+    public $entryid;
12
+    public $starttime;
13
+    public $length;
14
+    public $store;
15
+    public $proptags;
16
+
17
+    /**
18
+     * Constructor.
19
+     *
20
+     * @param mapi_session $session  MAPI Session
21
+     * @param mapi_folder  $calendar Calendar to publish
22
+     * @param string       $entryid  AddressBook Entry ID for the user we're publishing for
23
+     * @param mixed        $store
24
+     */
25
+    public function __construct($session, $store, $calendar, $entryid) {
26
+        $properties["entryid"] = PR_ENTRYID;
27
+        $properties["parent_entryid"] = PR_PARENT_ENTRYID;
28
+        $properties["message_class"] = PR_MESSAGE_CLASS;
29
+        $properties["icon_index"] = PR_ICON_INDEX;
30
+        $properties["subject"] = PR_SUBJECT;
31
+        $properties["display_to"] = PR_DISPLAY_TO;
32
+        $properties["importance"] = PR_IMPORTANCE;
33
+        $properties["sensitivity"] = PR_SENSITIVITY;
34
+        $properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:0x820d";
35
+        $properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:0x820e";
36
+        $properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:0x8223";
37
+        $properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:0x8216";
38
+        $properties["busystatus"] = "PT_LONG:PSETID_Appointment:0x8205";
39
+        $properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
40
+        $properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:0x8215";
41
+        $properties["private"] = "PT_BOOLEAN:PSETID_Common:0x8506";
42
+        $properties["meeting"] = "PT_LONG:PSETID_Appointment:0x8217";
43
+        $properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8235";
44
+        $properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:0x8236";
45
+        $properties["location"] = "PT_STRING8:PSETID_Appointment:0x8208";
46
+        $properties["duration"] = "PT_LONG:PSETID_Appointment:0x8213";
47
+        $properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
48
+        $properties["reminder"] = "PT_BOOLEAN:PSETID_Common:0x8503";
49
+        $properties["reminder_minutes"] = "PT_LONG:PSETID_Common:0x8501";
50
+        $properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
51
+        $properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
52
+        $properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
53
+        $properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:0x8502";
54
+        $properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
55
+        $properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
56
+        $properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:0x8228";
57
+        $properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:0x8233";
58
+        $this->proptags = getPropIdsFromStrings($store, $properties);
59
+
60
+        $this->session = $session;
61
+        $this->calendar = $calendar;
62
+        $this->entryid = $entryid;
63
+        $this->store = $store;
64
+    }
65
+
66
+    /**
67
+     * Function is used to get the calendar data based on give date range.
68
+     *
69
+     * @param timestamp $starttime time from which to get the calendar data
70
+     * @param timestamp $length    time up till now get the calendar data
71
+     *
72
+     * @return array return the calendar data array
73
+     */
74
+    public function getCalendarData($starttime, $length) {
75
+        $start = $starttime;
76
+        $end = $length;
77
+
78
+        // Get all the items in the calendar that we need
79
+
80
+        $calendaritems = [];
81
+
82
+        $restrict = [
83
+            RES_OR,
84
+            [
85
+                // OR
86
+                // (item[start] >= start && item[start] <= end)
87
+                [
88
+                    RES_AND,
89
+                    [
90
+                        [
91
+                            RES_PROPERTY,
92
+                            [
93
+                                RELOP => RELOP_GE,
94
+                                ULPROPTAG => $this->proptags["startdate"],
95
+                                VALUE => $start,
96
+                            ],
97
+                        ],
98
+                        [
99
+                            RES_PROPERTY,
100
+                            [
101
+                                RELOP => RELOP_LE,
102
+                                ULPROPTAG => $this->proptags["startdate"],
103
+                                VALUE => $end,
104
+                            ],
105
+                        ],
106
+                    ],
107
+                ],
108
+                // OR
109
+                // (item[end]   >= start && item[end]   <= end)
110
+                [
111
+                    RES_AND,
112
+                    [
113
+                        [
114
+                            RES_PROPERTY,
115
+                            [
116
+                                RELOP => RELOP_GE,
117
+                                ULPROPTAG => $this->proptags["duedate"],
118
+                                VALUE => $start,
119
+                            ],
120
+                        ],
121
+                        [
122
+                            RES_PROPERTY,
123
+                            [
124
+                                RELOP => RELOP_LE,
125
+                                ULPROPTAG => $this->proptags["duedate"],
126
+                                VALUE => $end,
127
+                            ],
128
+                        ],
129
+                    ],
130
+                ],
131
+                // OR
132
+                // (item[start] <  start && item[end]   >  end)
133
+                [
134
+                    RES_AND,
135
+                    [
136
+                        [
137
+                            RES_PROPERTY,
138
+                            [
139
+                                RELOP => RELOP_LT,
140
+                                ULPROPTAG => $this->proptags["startdate"],
141
+                                VALUE => $start,
142
+                            ],
143
+                        ],
144
+                        [
145
+                            RES_PROPERTY,
146
+                            [
147
+                                RELOP => RELOP_GT,
148
+                                ULPROPTAG => $this->proptags["duedate"],
149
+                                VALUE => $end,
150
+                            ],
151
+                        ],
152
+                    ],
153
+                ],
154
+                // OR
155
+                [
156
+                    RES_OR,
157
+                    [
158
+                        // OR
159
+                        // (EXIST(ecurrence_enddate_property) && item[isRecurring] == true && item[end] >= start)
160
+                        [
161
+                            RES_AND,
162
+                            [
163
+                                [
164
+                                    RES_EXIST,
165
+                                    [ULPROPTAG => $this->proptags["enddate_recurring"]],
166
+                                ],
167
+                                [
168
+                                    RES_PROPERTY,
169
+                                    [
170
+                                        RELOP => RELOP_EQ,
171
+                                        ULPROPTAG => $this->proptags["recurring"],
172
+                                        VALUE => true,
173
+                                    ],
174
+                                ],
175
+                                [
176
+                                    RES_PROPERTY,
177
+                                    [
178
+                                        RELOP => RELOP_GE,
179
+                                        ULPROPTAG => $this->proptags["enddate_recurring"],
180
+                                        VALUE => $start,
181
+                                    ],
182
+                                ],
183
+                            ],
184
+                        ],
185
+                        // OR
186
+                        // (!EXIST(ecurrence_enddate_property) && item[isRecurring] == true && item[start] <= end)
187
+                        [
188
+                            RES_AND,
189
+                            [
190
+                                [
191
+                                    RES_NOT,
192
+                                    [
193
+                                        [
194
+                                            RES_EXIST,
195
+                                            [ULPROPTAG => $this->proptags["enddate_recurring"],
196
+                                            ],
197
+                                        ],
198
+                                    ],
199
+                                ],
200
+                                [
201
+                                    RES_PROPERTY,
202
+                                    [
203
+                                        RELOP => RELOP_LE,
204
+                                        ULPROPTAG => $this->proptags["startdate"],
205
+                                        VALUE => $end,
206
+                                    ],
207
+                                ],
208
+                                [
209
+                                    RES_PROPERTY,
210
+                                    [
211
+                                        RELOP => RELOP_EQ,
212
+                                        ULPROPTAG => $this->proptags["recurring"],
213
+                                        VALUE => true,
214
+                                    ],
215
+                                ],
216
+                            ],
217
+                        ],
218
+                    ],
219
+                ], // EXISTS OR
220
+            ],
221
+        ];        // global OR
222
+
223
+        $contents = mapi_folder_getcontentstable($this->calendar);
224
+        mapi_table_restrict($contents, $restrict);
225
+
226
+        while (1) {
227
+            $rows = mapi_table_queryrows($contents, array_values($this->proptags), 0, 50);
228
+
229
+            if (!is_array($rows)) {
230
+                break;
231
+            }
232
+
233
+            if (empty($rows)) {
234
+                break;
235
+            }
236
+
237
+            foreach ($rows as $row) {
238
+                $occurrences = [];
239
+                if (isset($row[$this->proptags['recurring']]) && $row[$this->proptags['recurring']]) {
240
+                    $recur = new Recurrence($this->store, $row);
241
+
242
+                    $occurrences = $recur->getItems($starttime, $length);
243
+                }
244
+                else {
245
+                    $occurrences[] = $row;
246
+                }
247
+
248
+                $calendaritems = array_merge($calendaritems, $occurrences);
249
+            }
250
+        }
251
+
252
+        // $calendaritems now contains all the calendar items in the specified time
253
+        // frame. We now need to merge these into a flat array of begin/end/status
254
+        // objects. This also filters out all the 'free' items (status 0)
255
+        return $this->mergeItemsFB($calendaritems);
256
+    }
257
+
258
+    /**
259
+     * Publishes Free/Busy information of user.
260
+     *
261
+     * @param timestamp $starttime Time from which to publish data  (usually now)
262
+     * @param timestamp $length    Time of seconds from $starttime we should publish
263
+     * @param mixed     $start
264
+     * @param mixed     $end
265
+     */
266
+    public function publishFB($start, $end) {
267
+        $freebusy = $this->getCalendarData($start, $end);
268
+
269
+        // Get the FB interface
270
+        try {
271
+            $fbsupport = mapi_freebusysupport_open($this->session, $this->store);
272
+        }
273
+        catch (MAPIException $e) {
274
+            if ($e->getCode() == MAPI_E_NOT_FOUND) {
275
+                $e->setHandled();
276
+                SLog::Write(LOGLEVEL_WARN, "Error in opening freebusysupport object.");
277
+            }
278
+        }
279
+
280
+        // Open updater for this user
281
+        if (isset($fbsupport) && $fbsupport) {
282
+            $updaters = mapi_freebusysupport_loadupdate($fbsupport, [$this->entryid]);
283
+
284
+            $updater = $updaters[0];
285
+
286
+            // Send the data
287
+            mapi_freebusyupdate_reset($updater);
288
+            mapi_freebusyupdate_publish($updater, $freebusy);
289
+            mapi_freebusyupdate_savechanges($updater, $start - 24 * 60 * 60, $end);
290
+
291
+            // We're finished
292
+            mapi_freebusysupport_close($fbsupport);
293
+        }
294
+        else {
295
+            SLog::Write(LOGLEVEL_WARN, "FreeBusyPublish is not available");
296
+        }
297
+    }
298
+
299
+    /**
300
+     * Sorts by timestamp, if equal, then end before start.
301
+     *
302
+     * @param mixed $a
303
+     * @param mixed $b
304
+     */
305
+    public function cmp($a, $b) {
306
+        if ($a["time"] == $b["time"]) {
307
+            if ($a["type"] < $b["type"]) {
308
+                return 1;
309
+            }
310
+            if ($a["type"] > $b["type"]) {
311
+                return -1;
312
+            }
313
+
314
+            return 0;
315
+        }
316
+
317
+        return $a["time"] > $b["time"] ? 1 : -1;
318
+    }
319
+
320
+    /**
321
+     * Function mergeItems.
322
+     *
323
+     * @author Steve Hardy
324
+     *
325
+     * @param mixed $items
326
+     */
327
+    public function mergeItemsFB($items) {
328
+        $merged = [];
329
+        $timestamps = [];
330
+        $csubj = [];
331
+        $cbusy = [];
332
+        $level = 0;
333
+        $laststart = null;
334
+
335
+        foreach ($items as $item) {
336
+            $ts["type"] = 0;
337
+            $ts["time"] = $item[$this->proptags["startdate"]];
338
+            $ts["subject"] = $item[PR_SUBJECT];
339
+            $ts["status"] = (isset($item[$this->proptags["busystatus"]])) ? $item[$this->proptags["busystatus"]] : fbFree; // ZP-197
340
+            $timestamps[] = $ts;
341
+
342
+            $ts["type"] = 1;
343
+            $ts["time"] = $item[$this->proptags["duedate"]];
344
+            $ts["subject"] = $item[PR_SUBJECT];
345
+            $ts["status"] = (isset($item[$this->proptags["busystatus"]])) ? $item[$this->proptags["busystatus"]] : fbFree; // ZP-197
346
+            $timestamps[] = $ts;
347
+        }
348
+
349
+        usort($timestamps, [$this, "cmp"]);
350
+        $laststart = 0; // seb added
351
+
352
+        foreach ($timestamps as $ts) {
353
+            switch ($ts["type"]) {
354
+                case 0: // Start
355
+                    if ($level != 0 && $laststart != $ts["time"]) {
356
+                        $newitem["start"] = $laststart;
357
+                        $newitem["end"] = $ts["time"];
358
+                        $newitem["subject"] = join(",", $csubj);
359
+                        $newitem["status"] = !empty($cbusy) ? max($cbusy) : 0;
360
+                        if ($newitem["status"] > 0) {
361
+                            $merged[] = $newitem;
362
+                        }
363
+                    }
364
+
365
+                    ++$level;
366
+
367
+                    $csubj[] = $ts["subject"];
368
+                    $cbusy[] = $ts["status"];
369
+
370
+                    $laststart = $ts["time"];
371
+
372
+                    break;
373
+
374
+                case 1: // End
375
+                    if ($laststart != $ts["time"]) {
376
+                        $newitem["start"] = $laststart;
377
+                        $newitem["end"] = $ts["time"];
378
+                        $newitem["subject"] = join(",", $csubj);
379
+                        $newitem["status"] = !empty($cbusy) ? max($cbusy) : 0;
380
+                        if ($newitem["status"] > 0) {
381
+                            $merged[] = $newitem;
382
+                        }
383
+                    }
384
+
385
+                    --$level;
386
+
387
+                    array_splice($csubj, array_search($ts["subject"], $csubj, 1), 1);
388
+                    array_splice($cbusy, array_search($ts["status"], $cbusy, 1), 1);
389
+
390
+                    $laststart = $ts["time"];
391
+
392
+                    break;
393
+            }
394
+        }
395
+
396
+        return $merged;
397
+    }
398 398
 }
Please login to merge, or discard this patch.
mapi/class.baserecurrence.php 1 patch
Indentation   +1978 added lines, -1978 removed lines patch added patch discarded remove patch
@@ -5,1983 +5,1983 @@
 block discarded – undo
5 5
  * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
6 6
  */
7 7
 
8
-	/**
9
-	 * BaseRecurrence
10
-	 * this class is superclass for recurrence for appointments and tasks. This class provides all
11
-	 * basic features of recurrence.
12
-	 */
13
-	class BaseRecurrence {
14
-		/**
15
-		 * @var object Mapi Message Store (may be null if readonly)
16
-		 */
17
-		public $store;
18
-
19
-		/**
20
-		 * @var object Mapi Message (may be null if readonly)
21
-		 */
22
-		public $message;
23
-
24
-		/**
25
-		 * @var array Message Properties
26
-		 */
27
-		public $messageprops;
28
-
29
-		/**
30
-		 * @var array list of property tags
31
-		 */
32
-		public $proptags;
33
-
34
-		/**
35
-		 * @var recurrence data of this calendar item
36
-		 */
37
-		public $recur;
38
-
39
-		/**
40
-		 * @var Timezone data of this calendar item
41
-		 */
42
-		public $tz;
43
-
44
-		/**
45
-		 * Constructor.
46
-		 *
47
-		 * @param resource $store      MAPI Message Store Object
48
-		 * @param resource $message    the MAPI (appointment) message
49
-		 * @param array    $properties the list of MAPI properties the message has
50
-		 */
51
-		public function __construct($store, $message) {
52
-			$this->store = $store;
53
-
54
-			if (is_array($message)) {
55
-				$this->messageprops = $message;
56
-			}
57
-			else {
58
-				$this->message = $message;
59
-				$this->messageprops = mapi_getprops($this->message, $this->proptags);
60
-			}
61
-
62
-			if (isset($this->messageprops[$this->proptags["recurring_data"]])) {
63
-				// There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface
64
-				if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255) {
65
-					$this->getFullRecurrenceBlob();
66
-				}
67
-
68
-				$this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]);
69
-			}
70
-			if (isset($this->proptags["timezone_data"], $this->messageprops[$this->proptags["timezone_data"]])) {
71
-				$this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]);
72
-			}
73
-		}
74
-
75
-		public function getRecurrence() {
76
-			return $this->recur;
77
-		}
78
-
79
-		public function getFullRecurrenceBlob() {
80
-			$message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]);
81
-
82
-			$recurrBlob = '';
83
-			$stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
84
-			$stat = mapi_stream_stat($stream);
85
-
86
-			for ($i = 0; $i < $stat['cb']; $i += 1024) {
87
-				$recurrBlob .= mapi_stream_read($stream, 1024);
88
-			}
89
-
90
-			if (!empty($recurrBlob)) {
91
-				$this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob;
92
-			}
93
-		}
94
-
95
-		/**
96
-		 * Function for parsing the Recurrence value of a Calendar item.
97
-		 *
98
-		 * Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the
99
-		 * data to this function
100
-		 *
101
-		 * Returns a structure containing the data:
102
-		 *
103
-		 * type        - type of recurrence: day=10, week=11, month=12, year=13
104
-		 * subtype    - type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday)
105
-		 * start    - unix timestamp of first occurrence
106
-		 * end        - unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1
107
-		 * numoccur     - occurrences (may be very large when there is no end data)
108
-		 *
109
-		 * then, for each type:
110
-		 *
111
-		 * Daily:
112
-		 *  everyn    - every [everyn] days in minutes
113
-		 *  regen    - regenerating event (like tasks)
114
-		 *
115
-		 * Weekly:
116
-		 *  everyn    - every [everyn] weeks in weeks
117
-		 *  regen    - regenerating event (like tasks)
118
-		 *  weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
119
-		 *
120
-		 * Monthly:
121
-		 *  everyn    - every [everyn] months
122
-		 *  regen    - regenerating event (like tasks)
123
-		 *
124
-		 *  subtype 2:
125
-		 *      monthday - on day [monthday] of the month
126
-		 *
127
-		 *  subtype 3:
128
-		 *      weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
129
-		 *   nday    - on [nday]'th [weekdays] of the month
130
-		 *
131
-		 * Yearly:
132
-		 *  everyn    - every [everyn] months (12, 24, 36, ...)
133
-		 *  month    - in month [month] (although the month is encoded in minutes since the startning of the year ........)
134
-		 *  regen    - regenerating event (like tasks)
135
-		 *
136
-		 *  subtype 2:
137
-		 *   monthday - on day [monthday] of the month
138
-		 *
139
-		 *  subtype 3:
140
-		 *   weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
141
-		 *      nday    - on [nday]'th [weekdays] of the month [month]
142
-		 *
143
-		 * @param string $rdata Binary string
144
-		 *
145
-		 * @return array recurrence data
146
-		 */
147
-		public function parseRecurrence($rdata) {
148
-			if (strlen($rdata) < 10) {
149
-				return;
150
-			}
151
-
152
-			$ret["changed_occurrences"] = [];
153
-			$ret["deleted_occurrences"] = [];
154
-
155
-			$data = unpack("Vconst1/Crtype/Cconst2/Vrtype2", $rdata);
156
-
157
-			$ret["type"] = $data["rtype"];
158
-			$ret["subtype"] = $data["rtype2"];
159
-			$rdata = substr($rdata, 10);
160
-
161
-			switch ($data["rtype"]) {
162
-				case 0x0A:
163
-					// Daily
164
-					if (strlen($rdata) < 12) {
165
-						return $ret;
166
-					}
167
-
168
-					$data = unpack("Vunknown/Veveryn/Vregen", $rdata);
169
-					$ret["everyn"] = $data["everyn"];
170
-					$ret["regen"] = $data["regen"];
171
-
172
-					switch ($ret["subtype"]) {
173
-						case 0:
174
-							$rdata = substr($rdata, 12);
175
-
176
-							break;
177
-
178
-						case 1:
179
-							$rdata = substr($rdata, 16);
180
-
181
-							break;
182
-					}
183
-
184
-					break;
185
-
186
-				case 0x0B:
187
-					// Weekly
188
-					if (strlen($rdata) < 16) {
189
-						return $ret;
190
-					}
191
-
192
-					$data = unpack("Vconst1/Veveryn/Vregen", $rdata);
193
-					$rdata = substr($rdata, 12);
194
-
195
-					$ret["everyn"] = $data["everyn"];
196
-					$ret["regen"] = $data["regen"];
197
-					$ret["weekdays"] = 0;
198
-
199
-					if ($data["regen"] == 0) {
200
-						$data = unpack("Vweekdays", $rdata);
201
-						$rdata = substr($rdata, 4);
202
-
203
-						$ret["weekdays"] = $data["weekdays"];
204
-					}
205
-
206
-					break;
207
-
208
-				case 0x0C:
209
-					// Monthly
210
-					if (strlen($rdata) < 16) {
211
-						return $ret;
212
-					}
213
-
214
-					$data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata);
215
-
216
-					$ret["everyn"] = $data["everyn"];
217
-					$ret["regen"] = $data["regen"];
218
-
219
-					if ($ret["subtype"] == 3) {
220
-						$ret["weekdays"] = $data["monthday"];
221
-					}
222
-					else {
223
-						$ret["monthday"] = $data["monthday"];
224
-					}
225
-
226
-					$rdata = substr($rdata, 16);
227
-
228
-					if ($ret["subtype"] == 3) {
229
-						$data = unpack("Vnday", $rdata);
230
-						$ret["nday"] = $data["nday"];
231
-						$rdata = substr($rdata, 4);
232
-					}
233
-
234
-					break;
235
-
236
-				case 0x0D:
237
-					// Yearly
238
-					if (strlen($rdata) < 16) {
239
-						return $ret;
240
-					}
241
-
242
-					$data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata);
243
-
244
-					$ret["month"] = $data["month"];
245
-					$ret["everyn"] = $data["everyn"];
246
-					$ret["regen"] = $data["regen"];
247
-
248
-					if ($ret["subtype"] == 3) {
249
-						$ret["weekdays"] = $data["monthday"];
250
-					}
251
-					else {
252
-						$ret["monthday"] = $data["monthday"];
253
-					}
254
-
255
-					$rdata = substr($rdata, 16);
256
-
257
-					if ($ret["subtype"] == 3) {
258
-						$data = unpack("Vnday", $rdata);
259
-						$ret["nday"] = $data["nday"];
260
-						$rdata = substr($rdata, 4);
261
-					}
262
-
263
-					break;
264
-			}
265
-
266
-			if (strlen($rdata) < 16) {
267
-				return $ret;
268
-			}
269
-
270
-			$data = unpack("Cterm/C3const1/Vnumoccur/Vconst2/Vnumexcept", $rdata);
271
-
272
-			$rdata = substr($rdata, 16);
273
-
274
-			$ret["term"] = $data["term"];
275
-			$ret["numoccur"] = $data["numoccur"];
276
-			$ret["numexcept"] = $data["numexcept"];
277
-
278
-			// exc_base_dates are *all* the base dates that have been either deleted or modified
279
-			$exc_base_dates = [];
280
-			for ($i = 0; $i < $ret["numexcept"]; ++$i) {
281
-				if (strlen($rdata) < 4) {
282
-					// We shouldn't arrive here, because that implies
283
-					// numexcept does not match the amount of data
284
-					// which is available for the exceptions.
285
-					return $ret;
286
-				}
287
-				$data = unpack("Vbasedate", $rdata);
288
-				$rdata = substr($rdata, 4);
289
-				$exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]);
290
-			}
291
-
292
-			if (strlen($rdata) < 4) {
293
-				return $ret;
294
-			}
295
-
296
-			$data = unpack("Vnumexceptmod", $rdata);
297
-			$rdata = substr($rdata, 4);
298
-
299
-			$ret["numexceptmod"] = $data["numexceptmod"];
300
-
301
-			// exc_changed are the base dates of *modified* occurrences. exactly what is modified
302
-			// is in the attachments *and* in the data further down this function.
303
-			$exc_changed = [];
304
-			for ($i = 0; $i < $ret["numexceptmod"]; ++$i) {
305
-				if (strlen($rdata) < 4) {
306
-					// We shouldn't arrive here, because that implies
307
-					// numexceptmod does not match the amount of data
308
-					// which is available for the exceptions.
309
-					return $ret;
310
-				}
311
-				$data = unpack("Vstartdate", $rdata);
312
-				$rdata = substr($rdata, 4);
313
-				$exc_changed[] = $this->recurDataToUnixData($data["startdate"]);
314
-			}
315
-
316
-			if (strlen($rdata) < 8) {
317
-				return $ret;
318
-			}
319
-
320
-			$data = unpack("Vstart/Vend", $rdata);
321
-			$rdata = substr($rdata, 8);
322
-
323
-			$ret["start"] = $this->recurDataToUnixData($data["start"]);
324
-			$ret["end"] = $this->recurDataToUnixData($data["end"]);
325
-
326
-			// this is where task recurrence stop
327
-			if (strlen($rdata) < 16) {
328
-				return $ret;
329
-			}
330
-
331
-			$data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata);
332
-			$rdata = substr($rdata, 16);
333
-
334
-			$ret["startocc"] = $data["startmin"];
335
-			$ret["endocc"] = $data["endmin"];
336
-			$writerversion = $data["writerversion"];
337
-
338
-			$data = unpack("vnumber", $rdata);
339
-			$rdata = substr($rdata, 2);
340
-
341
-			$nexceptions = $data["number"];
342
-			$exc_changed_details = [];
343
-
344
-			// Parse n modified exceptions
345
-			for ($i = 0; $i < $nexceptions; ++$i) {
346
-				$item = [];
347
-
348
-				// Get exception startdate, enddate and basedate (the date at which the occurrence would have started)
349
-				$data = unpack("Vstartdate/Venddate/Vbasedate", $rdata);
350
-				$rdata = substr($rdata, 12);
351
-
352
-				// Convert recurtimestamp to unix timestamp
353
-				$startdate = $this->recurDataToUnixData($data["startdate"]);
354
-				$enddate = $this->recurDataToUnixData($data["enddate"]);
355
-				$basedate = $this->recurDataToUnixData($data["basedate"]);
356
-
357
-				// Set the right properties
358
-				$item["basedate"] = $this->dayStartOf($basedate);
359
-				$item["start"] = $startdate;
360
-				$item["end"] = $enddate;
361
-
362
-				$data = unpack("vbitmask", $rdata);
363
-				$rdata = substr($rdata, 2);
364
-				$item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions
365
-
366
-				// Bitmask to verify what properties are changed
367
-				$bitmask = $data["bitmask"];
368
-
369
-				// ARO_SUBJECT: 0x0001
370
-				// Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject
371
-				if (($bitmask & (1 << 0))) {
372
-					$data = unpack("vnull_length/vlength", $rdata);
373
-					$rdata = substr($rdata, 4);
374
-
375
-					$length = $data["length"];
376
-					$item["subject"] = ""; // Normalized subject
377
-					for ($j = 0; $j < $length && strlen($rdata); ++$j) {
378
-						$data = unpack("Cchar", $rdata);
379
-						$rdata = substr($rdata, 1);
380
-
381
-						$item["subject"] .= chr($data["char"]);
382
-					}
383
-				}
384
-
385
-				// ARO_MEETINGTYPE: 0x0002
386
-				if (($bitmask & (1 << 1))) {
387
-					$rdata = substr($rdata, 4);
388
-					// Attendees modified: no data here (only in attachment)
389
-				}
390
-
391
-				// ARO_REMINDERDELTA: 0x0004
392
-				// Look for field: ReminderDelta (4b)
393
-				if (($bitmask & (1 << 2))) {
394
-					$data = unpack("Vremind_before", $rdata);
395
-					$rdata = substr($rdata, 4);
396
-
397
-					$item["remind_before"] = $data["remind_before"];
398
-				}
399
-
400
-				// ARO_REMINDER: 0x0008
401
-				// Look field: ReminderSet (4b)
402
-				if (($bitmask & (1 << 3))) {
403
-					$data = unpack("Vreminder_set", $rdata);
404
-					$rdata = substr($rdata, 4);
405
-
406
-					$item["reminder_set"] = $data["reminder_set"];
407
-				}
408
-
409
-				// ARO_LOCATION: 0x0010
410
-				// Look for fields: LocationLength (2b), LocationLength2 (2b) and Location
411
-				// Similar to ARO_SUBJECT above.
412
-				if (($bitmask & (1 << 4))) {
413
-					$data = unpack("vnull_length/vlength", $rdata);
414
-					$rdata = substr($rdata, 4);
415
-
416
-					$item["location"] = "";
417
-
418
-					$length = $data["length"];
419
-					$data = substr($rdata, 0, $length);
420
-					$rdata = substr($rdata, $length);
421
-
422
-					$item["location"] .= $data;
423
-				}
424
-
425
-				// ARO_BUSYSTATUS: 0x0020
426
-				// Look for field: BusyStatus (4b)
427
-				if (($bitmask & (1 << 5))) {
428
-					$data = unpack("Vbusystatus", $rdata);
429
-					$rdata = substr($rdata, 4);
430
-
431
-					$item["busystatus"] = $data["busystatus"];
432
-				}
433
-
434
-				// ARO_ATTACHMENT: 0x0040
435
-				if (($bitmask & (1 << 6))) {
436
-					// no data: RESERVED
437
-					$rdata = substr($rdata, 4);
438
-				}
439
-
440
-				// ARO_SUBTYPE: 0x0080
441
-				// Look for field: SubType (4b). Determines whether it is an allday event.
442
-				if (($bitmask & (1 << 7))) {
443
-					$data = unpack("Vallday", $rdata);
444
-					$rdata = substr($rdata, 4);
445
-
446
-					$item["alldayevent"] = $data["allday"];
447
-				}
448
-
449
-				// ARO_APPTCOLOR: 0x0100
450
-				// Look for field: AppointmentColor (4b)
451
-				if (($bitmask & (1 << 8))) {
452
-					$data = unpack("Vlabel", $rdata);
453
-					$rdata = substr($rdata, 4);
454
-
455
-					$item["label"] = $data["label"];
456
-				}
457
-
458
-				// ARO_EXCEPTIONAL_BODY: 0x0200
459
-				if (($bitmask & (1 << 9))) {
460
-					// Notes or Attachments modified: no data here (only in attachment)
461
-				}
462
-
463
-				array_push($exc_changed_details, $item);
464
-			}
465
-
466
-			/**
467
-			 * We now have $exc_changed, $exc_base_dates and $exc_changed_details
468
-			 * We will ignore $exc_changed, as this information is available in $exc_changed_details
469
-			 * also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item
470
-			 * has been deleted.
471
-			 */
472
-
473
-			// Find deleted occurrences
474
-			$deleted_occurrences = [];
475
-
476
-			foreach ($exc_base_dates as $base_date) {
477
-				$found = false;
478
-
479
-				foreach ($exc_changed_details as $details) {
480
-					if ($details["basedate"] == $base_date) {
481
-						$found = true;
482
-
483
-						break;
484
-					}
485
-				}
486
-				if (!$found) {
487
-					// item was not in exc_changed_details, so it must be deleted
488
-					$deleted_occurrences[] = $base_date;
489
-				}
490
-			}
491
-
492
-			$ret["deleted_occurrences"] = $deleted_occurrences;
493
-			$ret["changed_occurrences"] = $exc_changed_details;
494
-
495
-			// enough data for normal exception (no extended data)
496
-			if (strlen($rdata) < 16) {
497
-				return $ret;
498
-			}
499
-
500
-			$data = unpack("Vreservedsize", $rdata);
501
-			$rdata = substr($rdata, 4 + $data["reservedsize"]);
502
-
503
-			for ($i = 0; $i < $nexceptions; ++$i) {
504
-				// subject and location in ucs-2 to utf-8
505
-				if ($writerversion >= 0x3009) {
506
-					$data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4
507
-					$rdata = substr($rdata, 4 + $data["size"]);
508
-				}
509
-
510
-				$data = unpack("Vreservedsize", $rdata);
511
-				$rdata = substr($rdata, 4 + $data["reservedsize"]);
512
-
513
-				// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
514
-				if ($exc_changed_details[$i]["bitmask"] & 0x11) {
515
-					$data = unpack("Vstart/Vend/Vorig", $rdata);
516
-					$rdata = substr($rdata, 4 * 3);
517
-
518
-					$exc_changed_details[$i]["ex_start_datetime"] = $data["start"];
519
-					$exc_changed_details[$i]["ex_end_datetime"] = $data["end"];
520
-					$exc_changed_details[$i]["ex_orig_date"] = $data["orig"];
521
-				}
522
-
523
-				// ARO_SUBJECT
524
-				if ($exc_changed_details[$i]["bitmask"] & 0x01) {
525
-					// decode ucs2 string to utf-8
526
-					$data = unpack("vlength", $rdata);
527
-					$rdata = substr($rdata, 2);
528
-					$length = $data["length"];
529
-					$data = substr($rdata, 0, $length * 2);
530
-					$rdata = substr($rdata, $length * 2);
531
-					$subject = iconv("UCS-2LE", "UTF-8", $data);
532
-					// replace subject with unicode subject
533
-					$exc_changed_details[$i]["subject"] = $subject;
534
-				}
535
-
536
-				// ARO_LOCATION
537
-				if ($exc_changed_details[$i]["bitmask"] & 0x10) {
538
-					// decode ucs2 string to utf-8
539
-					$data = unpack("vlength", $rdata);
540
-					$rdata = substr($rdata, 2);
541
-					$length = $data["length"];
542
-					$data = substr($rdata, 0, $length * 2);
543
-					$rdata = substr($rdata, $length * 2);
544
-					$location = iconv("UCS-2LE", "UTF-8", $data);
545
-					// replace subject with unicode subject
546
-					$exc_changed_details[$i]["location"] = $location;
547
-				}
548
-
549
-				// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
550
-				if ($exc_changed_details[$i]["bitmask"] & 0x11) {
551
-					$data = unpack("Vreservedsize", $rdata);
552
-					$rdata = substr($rdata, 4 + $data["reservedsize"]);
553
-				}
554
-			}
555
-
556
-			// update with extended data
557
-			$ret["changed_occurrences"] = $exc_changed_details;
558
-
559
-			return $ret;
560
-		}
561
-
562
-		/**
563
-		 * Saves the recurrence data to the recurrence property.
564
-		 */
565
-		public function saveRecurrence() {
566
-			// Only save if a message was passed
567
-			if (!isset($this->message)) {
568
-				return;
569
-			}
570
-
571
-			// Abort if no recurrence was set
572
-			if (!isset($this->recur["type"], $this->recur["subtype"], $this->recur["start"], $this->recur["end"], $this->recur["startocc"], $this->recur["endocc"])) {
573
-				return;
574
-			}
575
-
576
-			$rdata = pack("CCCCCCV", 0x04, 0x30, 0x04, 0x30, (int) $this->recur["type"], 0x20, (int) $this->recur["subtype"]);
577
-
578
-			$weekstart = 1; // monday
579
-			$forwardcount = 0;
580
-			$restocc = 0;
581
-			$dayofweek = (int) gmdate("w", (int) $this->recur["start"]); // 0 (for Sunday) through 6 (for Saturday)
582
-
583
-			$term = (int) $this->recur["type"];
584
-
585
-			switch ($term) {
586
-				case 0x0A:
587
-					// Daily
588
-					if (!isset($this->recur["everyn"])) {
589
-						return;
590
-					}
591
-
592
-					if ($this->recur["subtype"] == 1) {
593
-						// Daily every workday
594
-						$rdata .= pack("VVVV", (6 * 24 * 60), 1, 0, 0x3E);
595
-					}
596
-					else {
597
-						// Daily every N days (everyN in minutes)
598
-
599
-						$everyn = ((int) $this->recur["everyn"]) / 1440;
600
-
601
-						// Calc first occ
602
-						$firstocc = $this->unixDataToRecurData($this->recur["start"]) % ((int) $this->recur["everyn"]);
603
-
604
-						$rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], $this->recur["regen"] ? 1 : 0);
605
-					}
606
-
607
-					break;
608
-
609
-				case 0x0B:
610
-					// Weekly
611
-					if (!isset($this->recur["everyn"])) {
612
-						return;
613
-					}
614
-
615
-					if (!$this->recur["regen"] && !isset($this->recur["weekdays"])) {
616
-						return;
617
-					}
618
-
619
-					// No need to calculate startdate if sliding flag was set.
620
-					if (!$this->recur['regen']) {
621
-						// Calculate start date of recurrence
622
-
623
-						// Find the first day that matches one of the weekdays selected
624
-						$daycount = 0;
625
-						$dayskip = -1;
626
-						for ($j = 0; $j < 7; ++$j) {
627
-							if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
628
-								if ($dayskip == -1) {
629
-									$dayskip = $j;
630
-								}
631
-
632
-								++$daycount;
633
-							}
634
-						}
635
-
636
-						// $dayskip is the number of days to skip from the startdate until the first occurrence
637
-						// $daycount is the number of days per week that an occurrence occurs
638
-
639
-						$weekskip = 0;
640
-						if (($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek + $dayskip) > 6) {
641
-							$weekskip = 1;
642
-						}
643
-
644
-						// Check if the recurrence ends after a number of occurrences, in that case we must calculate the
645
-						// remaining occurrences based on the start of the recurrence.
646
-						if (((int) $this->recur["term"]) == 0x22) {
647
-							// $weekskip is the amount of weeks to skip from the startdate before the first occurrence
648
-							// $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that
649
-							// is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over
650
-							// (eg when numoccur = 2, and daycount = 1)
651
-							$forwardcount = floor((int) ($this->recur["numoccur"] - 1) / $daycount);
652
-
653
-							// $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one
654
-							// for the occurrence on the first day
655
-							$restocc = ((int) $this->recur["numoccur"]) - ($forwardcount * $daycount) - 1;
656
-
657
-							// $forwardcount is now the number of weeks we can go forward and still be inside the recurrence
658
-							$forwardcount *= (int) $this->recur["everyn"];
659
-						}
660
-
661
-						// The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week)
662
-						$this->recur["start"] = ((int) $this->recur["start"]) + ($dayskip * 24 * 60 * 60) + ($weekskip * (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60);
663
-					}
664
-
665
-					// Calc first occ
666
-					$firstocc = ($this->unixDataToRecurData($this->recur["start"])) % (((int) $this->recur["everyn"]) * 7 * 24 * 60);
667
-
668
-					$firstocc -= (((int) gmdate("w", (int) $this->recur["start"])) - 1) * 24 * 60;
669
-
670
-					if ($this->recur["regen"]) {
671
-						$rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], 1);
672
-					}
673
-					else {
674
-						$rdata .= pack("VVVV", $firstocc, (int) $this->recur["everyn"], 0, (int) $this->recur["weekdays"]);
675
-					}
676
-
677
-					break;
678
-
679
-				case 0x0C:
680
-					// Monthly
681
-				case 0x0D:
682
-					// Yearly
683
-					if (!isset($this->recur["everyn"])) {
684
-						return;
685
-					}
686
-					if ($term == 0x0D /* yearly */ && !isset($this->recur["month"])) {
687
-						return;
688
-					}
689
-
690
-					if ($term == 0x0C /* monthly */) {
691
-						$everyn = (int) $this->recur["everyn"];
692
-					}
693
-					else {
694
-						$everyn = $this->recur["regen"] ? ((int) $this->recur["everyn"]) * 12 : 12;
695
-					}
696
-
697
-					// Get montday/month/year of original start
698
-					$curmonthday = gmdate("j", (int) $this->recur["start"]);
699
-					$curyear = gmdate("Y", (int) $this->recur["start"]);
700
-					$curmonth = gmdate("n", (int) $this->recur["start"]);
701
-
702
-					// Check if the recurrence ends after a number of occurrences, in that case we must calculate the
703
-					// remaining occurrences based on the start of the recurrence.
704
-					if (((int) $this->recur["term"]) == 0x22) {
705
-						// $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus
706
-						// one to make sure there are always at least one occurrence left)
707
-						$forwardcount = ((((int) $this->recur["numoccur"]) - 1) * $everyn);
708
-					}
709
-
710
-					// Get month for yearly on D'th day of month M
711
-					if ($term == 0x0D /* yearly */) {
712
-						$selmonth = floor(((int) $this->recur["month"]) / (24 * 60 * 29)) + 1; // 1=jan, 2=feb, eg
713
-					}
714
-
715
-					switch ((int) $this->recur["subtype"]) {
716
-						// on D day of every M month
717
-						case 2:
718
-							if (!isset($this->recur["monthday"])) {
719
-								return;
720
-							}
721
-							// Recalc startdate
722
-
723
-							// Set on the right begin day
724
-
725
-							// Go the beginning of the month
726
-							$this->recur["start"] -= ($curmonthday - 1) * 24 * 60 * 60;
727
-							// Go the the correct month day
728
-							$this->recur["start"] += (((int) $this->recur["monthday"]) - 1) * 24 * 60 * 60;
729
-
730
-							// If the previous calculation gave us a start date different than the original start date, then we need to skip to the first occurrence
731
-							if (($term == 0x0C /* monthly */ && ((int) $this->recur["monthday"]) < $curmonthday) ||
732
-								($term == 0x0D /* yearly */ && ($selmonth != $curmonth || ($selmonth == $curmonth && ((int) $this->recur["monthday"]) < $curmonthday)))) {
733
-								if ($term == 0x0D /* yearly */) {
734
-									if ($curmonth > $selmonth) {// go to next occurrence in 'everyn' months minus difference in first occurrence and original date
735
-										$count = $everyn - ($curmonth - $selmonth);
736
-									}
737
-									elseif ($curmonth < $selmonth) {// go to next occurrence upto difference in first occurrence and original date
738
-										$count = $selmonth - $curmonth;
739
-									}
740
-									else {
741
-										// Go to next occurrence while recurrence start date is greater than occurrence date but within same month
742
-										if (((int) $this->recur["monthday"]) < $curmonthday) {
743
-											$count = $everyn;
744
-										}
745
-									}
746
-								}
747
-								else {
748
-									$count = $everyn; // Monthly, go to next occurrence in 'everyn' months
749
-								}
750
-
751
-								// Forward by $count months. This is done by getting the number of days in that month and forwarding that many days
752
-								for ($i = 0; $i < $count; ++$i) {
753
-									$this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth);
754
-
755
-									if ($curmonth == 12) {
756
-										++$curyear;
757
-										$curmonth = 0;
758
-									}
759
-									++$curmonth;
760
-								}
761
-							}
762
-
763
-							// "start" is now pointing to the first occurrence, except that it will overshoot if the
764
-							// month in which it occurs has less days than specified as the day of the month. So 31st
765
-							// of each month will overshoot in february (29 days). We compensate for that by checking
766
-							// if the day of the month we got is wrong, and then back up to the last day of the previous
767
-							// month.
768
-							if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
769
-								gmdate("j", ((int) $this->recur["start"])) < ((int) $this->recur["monthday"])) {
770
-								$this->recur["start"] -= gmdate("j", ((int) $this->recur["start"])) * 24 * 60 * 60;
771
-							}
772
-
773
-							// "start" is now the first occurrence
774
-
775
-							if ($term == 0x0C /* monthly */) {
776
-								// Calc first occ
777
-								$monthIndex = ((((12 % $everyn) * ((((int) gmdate("Y", $this->recur["start"])) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
778
-
779
-								$firstocc = 0;
780
-								for ($i = 0; $i < $monthIndex; ++$i) {
781
-									$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
782
-								}
783
-
784
-								$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
785
-							}
786
-							else {
787
-								// Calc first occ
788
-								$firstocc = 0;
789
-								$monthIndex = (int) gmdate("n", $this->recur["start"]);
790
-								for ($i = 1; $i < $monthIndex; ++$i) {
791
-									$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
792
-								}
793
-
794
-								$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
795
-							}
796
-
797
-							break;
798
-
799
-						case 3:
800
-							// monthly: on Nth weekday of every M month
801
-							// yearly: on Nth weekday of M month
802
-							if (!isset($this->recur["weekdays"], $this->recur["nday"])) {
803
-								return;
804
-							}
805
-
806
-							$weekdays = (int) $this->recur["weekdays"];
807
-							$nday = (int) $this->recur["nday"];
808
-
809
-							// Calc startdate
810
-							$monthbegindow = (int) $this->recur["start"];
811
-
812
-							if ($nday == 5) {
813
-								// Set date on the last day of the last month
814
-								$monthbegindow += (gmdate("t", $monthbegindow) - gmdate("j", $monthbegindow)) * 24 * 60 * 60;
815
-							}
816
-							else {
817
-								// Set on the first day of the month
818
-								$monthbegindow -= ((gmdate("j", $monthbegindow) - 1) * 24 * 60 * 60);
819
-							}
820
-
821
-							if ($term == 0x0D /* yearly */) {
822
-								// Set on right month
823
-								if ($selmonth < $curmonth) {
824
-									$tmp = 12 - $curmonth + $selmonth;
825
-								}
826
-								else {
827
-									$tmp = ($selmonth - $curmonth);
828
-								}
829
-
830
-								for ($i = 0; $i < $tmp; ++$i) {
831
-									$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
832
-
833
-									if ($curmonth == 12) {
834
-										++$curyear;
835
-										$curmonth = 0;
836
-									}
837
-									++$curmonth;
838
-								}
839
-							}
840
-							else {
841
-								// Check or you exist in the right month
842
-
843
-								$dayofweek = gmdate("w", $monthbegindow);
844
-								for ($i = 0; $i < 7; ++$i) {
845
-									if ($nday == 5 && (($dayofweek - $i) % 7 >= 0) && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
846
-										$day = gmdate("j", $monthbegindow) - $i;
847
-
848
-										break;
849
-									}
850
-									if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
851
-										$day = (($nday - 1) * 7) + ($i + 1);
852
-
853
-										break;
854
-									}
855
-								}
856
-
857
-								// Goto the next X month
858
-								if (isset($day) && ($day < gmdate("j", (int) $this->recur["start"]))) {
859
-									if ($nday == 5) {
860
-										$monthbegindow += 24 * 60 * 60;
861
-										if ($curmonth == 12) {
862
-											++$curyear;
863
-											$curmonth = 0;
864
-										}
865
-										++$curmonth;
866
-									}
867
-
868
-									for ($i = 0; $i < $everyn; ++$i) {
869
-										$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
870
-
871
-										if ($curmonth == 12) {
872
-											++$curyear;
873
-											$curmonth = 0;
874
-										}
875
-										++$curmonth;
876
-									}
877
-
878
-									if ($nday == 5) {
879
-										$monthbegindow -= 24 * 60 * 60;
880
-									}
881
-								}
882
-							}
883
-
884
-							// FIXME: weekstart?
885
-
886
-							$day = 0;
887
-							// Set start on the right day
888
-							$dayofweek = gmdate("w", $monthbegindow);
889
-							for ($i = 0; $i < 7; ++$i) {
890
-								if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
891
-									$day = $i;
892
-
893
-									break;
894
-								}
895
-								if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
896
-									$day = ($nday - 1) * 7 + ($i + 1);
897
-
898
-									break;
899
-								}
900
-							}
901
-							if ($nday == 5) {
902
-								$monthbegindow -= $day * 24 * 60 * 60;
903
-							}
904
-							else {
905
-								$monthbegindow += ($day - 1) * 24 * 60 * 60;
906
-							}
907
-
908
-							$firstocc = 0;
909
-
910
-							if ($term == 0x0C /* monthly */) {
911
-								// Calc first occ
912
-								$monthIndex = ((((12 % $everyn) * (((int) gmdate("Y", $this->recur["start"]) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
913
-
914
-								for ($i = 0; $i < $monthIndex; ++$i) {
915
-									$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
916
-								}
917
-
918
-								$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
919
-							}
920
-							else {
921
-								// Calc first occ
922
-								$monthIndex = (int) gmdate("n", $this->recur["start"]);
923
-
924
-								for ($i = 1; $i < $monthIndex; ++$i) {
925
-									$firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
926
-								}
927
-
928
-								$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
929
-							}
930
-
931
-							break;
932
-					}
933
-
934
-					break;
935
-			}
936
-
937
-			if (!isset($this->recur["term"])) {
938
-				return;
939
-			}
940
-
941
-			// Terminate
942
-			$term = (int) $this->recur["term"];
943
-			$rdata .= pack("CCCC", $term, 0x20, 0x00, 0x00);
944
-
945
-			switch ($term) {
946
-				// After the given enddate
947
-				case 0x21:
948
-					$rdata .= pack("V", 10);
949
-
950
-					break;
951
-				// After a number of times
952
-				case 0x22:
953
-					if (!isset($this->recur["numoccur"])) {
954
-						return;
955
-					}
956
-
957
-					$rdata .= pack("V", (int) $this->recur["numoccur"]);
958
-
959
-					break;
960
-				// Never ends
961
-				case 0x23:
962
-					$rdata .= pack("V", 0);
963
-
964
-					break;
965
-			}
966
-
967
-			// Strange little thing for the recurrence type "every workday"
968
-			if (((int) $this->recur["type"]) == 0x0B && ((int) $this->recur["subtype"]) == 1) {
969
-				$rdata .= pack("V", 1);
970
-			}
971
-			else { // Other recurrences
972
-				$rdata .= pack("V", 0);
973
-			}
974
-
975
-			// Exception data
976
-
977
-			// Get all exceptions
978
-			$deleted_items = $this->recur["deleted_occurrences"];
979
-			$changed_items = $this->recur["changed_occurrences"];
980
-
981
-			// Merge deleted and changed items into one list
982
-			$items = $deleted_items;
983
-
984
-			foreach ($changed_items as $changed_item) {
985
-				array_push($items, $changed_item["basedate"]);
986
-			}
987
-
988
-			sort($items);
989
-
990
-			// Add the merged list in to the rdata
991
-			$rdata .= pack("V", count($items));
992
-			foreach ($items as $item) {
993
-				$rdata .= pack("V", $this->unixDataToRecurData($item));
994
-			}
995
-
996
-			// Loop through the changed exceptions (not deleted)
997
-			$rdata .= pack("V", count($changed_items));
998
-			$items = [];
999
-
1000
-			foreach ($changed_items as $changed_item) {
1001
-				$items[] = $this->dayStartOf($changed_item["start"]);
1002
-			}
1003
-
1004
-			sort($items);
1005
-
1006
-			// Add the changed items list int the rdata
1007
-			foreach ($items as $item) {
1008
-				$rdata .= pack("V", $this->unixDataToRecurData($item));
1009
-			}
1010
-
1011
-			// Set start date
1012
-			$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["start"]));
1013
-
1014
-			// Set enddate
1015
-			switch ($term) {
1016
-				// After the given enddate
1017
-				case 0x21:
1018
-					$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1019
-
1020
-					break;
1021
-				// After a number of times
1022
-				case 0x22:
1023
-					// @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour
1024
-					$occenddate = (int) $this->recur["start"];
1025
-
1026
-					switch ((int) $this->recur["type"]) {
1027
-						case 0x0A: // daily
1028
-							if ($this->recur["subtype"] == 1) {
1029
-								// Daily every workday
1030
-								$restocc = (int) $this->recur["numoccur"];
1031
-
1032
-								// Get starting weekday
1033
-								$nowtime = $this->gmtime($occenddate);
1034
-								$j = $nowtime["tm_wday"];
1035
-
1036
-								while (1) {
1037
-									if (($j % 7) > 0 && ($j % 7) < 6) {
1038
-										--$restocc;
1039
-									}
1040
-
1041
-									++$j;
1042
-
1043
-									if ($restocc <= 0) {
1044
-										break;
1045
-									}
1046
-
1047
-									$occenddate += 24 * 60 * 60;
1048
-								}
1049
-							}
1050
-							else {
1051
-								// -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence)
1052
-								$occenddate += (((int) $this->recur["everyn"]) * 60 * (((int) $this->recur["numoccur"] - 1)));
1053
-							}
1054
-
1055
-							break;
1056
-
1057
-						case 0x0B: // weekly
1058
-							// Needed values
1059
-							// $forwardcount - number of weeks we can skip forward
1060
-							// $restocc - number of remaining occurrences after the week skip
1061
-
1062
-							// Add the weeks till the last item
1063
-							$occenddate += ($forwardcount * 7 * 24 * 60 * 60);
1064
-
1065
-							$dayofweek = gmdate("w", $occenddate);
1066
-
1067
-							// Loop through the last occurrences until we have had them all
1068
-							for ($j = 1; $restocc > 0; ++$j) {
1069
-								// Jump to the next week (which may be N weeks away) when going over the week boundary
1070
-								if ((($dayofweek + $j) % 7) == $weekstart) {
1071
-									$occenddate += (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60;
1072
-								}
1073
-
1074
-								// If this is a matching day, once less occurrence to process
1075
-								if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
1076
-									--$restocc;
1077
-								}
1078
-
1079
-								// Next day
1080
-								$occenddate += 24 * 60 * 60;
1081
-							}
1082
-
1083
-							break;
1084
-
1085
-						case 0x0C: // monthly
1086
-						case 0x0D: // yearly
1087
-							$curyear = gmdate("Y", (int) $this->recur["start"]);
1088
-							$curmonth = gmdate("n", (int) $this->recur["start"]);
1089
-							// $forwardcount = months
1090
-
1091
-							switch ((int) $this->recur["subtype"]) {
1092
-								case 2: // on D day of every M month
1093
-									while ($forwardcount > 0) {
1094
-										$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1095
-
1096
-										if ($curmonth >= 12) {
1097
-											$curmonth = 1;
1098
-											++$curyear;
1099
-										}
1100
-										else {
1101
-											++$curmonth;
1102
-										}
1103
-										--$forwardcount;
1104
-									}
1105
-
1106
-									// compensation between 28 and 31
1107
-									if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
1108
-										gmdate("j", $occenddate) < ((int) $this->recur["monthday"])) {
1109
-										if (gmdate("j", $occenddate) < 28) {
1110
-											$occenddate -= gmdate("j", $occenddate) * 24 * 60 * 60;
1111
-										}
1112
-										else {
1113
-											$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1114
-										}
1115
-									}
1116
-
1117
-									break;
1118
-
1119
-								case 3: // on Nth weekday of every M month
1120
-									$nday = (int) $this->recur["nday"]; // 1 tot 5
1121
-									$weekdays = (int) $this->recur["weekdays"];
1122
-
1123
-									while ($forwardcount > 0) {
1124
-										$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1125
-										if ($curmonth >= 12) {
1126
-											$curmonth = 1;
1127
-											++$curyear;
1128
-										}
1129
-										else {
1130
-											++$curmonth;
1131
-										}
1132
-
1133
-										--$forwardcount;
1134
-									}
1135
-
1136
-									if ($nday == 5) {
1137
-										// Set date on the last day of the last month
1138
-										$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1139
-									}
1140
-									else {
1141
-										// Set date on the first day of the last month
1142
-										$occenddate -= (gmdate("j", $occenddate) - 1) * 24 * 60 * 60;
1143
-									}
1144
-
1145
-									$dayofweek = gmdate("w", $occenddate);
1146
-									for ($i = 0; $i < 7; ++$i) {
1147
-										if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
1148
-											$occenddate -= $i * 24 * 60 * 60;
1149
-
1150
-											break;
1151
-										}
1152
-										if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
1153
-											$occenddate += ($i + (($nday - 1) * 7)) * 24 * 60 * 60;
1154
-
1155
-											break;
1156
-										}
1157
-									}
1158
-
1159
-								break; // case 3:
1160
-								}
1161
-
1162
-							break;
1163
-					}
1164
-
1165
-					if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX) {
1166
-						$occenddate = PHP_INT_MAX;
1167
-					}
1168
-
1169
-					$this->recur["end"] = $occenddate;
1170
-
1171
-					$rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1172
-
1173
-					break;
1174
-				// Never ends
1175
-				case 0x23:
1176
-				default:
1177
-					$this->recur["end"] = 0x7FFFFFFF; // max date -> 2038
1178
-					$rdata .= pack("V", 0x5AE980DF);
1179
-
1180
-					break;
1181
-			}
1182
-
1183
-			// UTC date
1184
-			$utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]);
1185
-			$utcend = $this->toGMT($this->tz, (int) $this->recur["end"]);
1186
-
1187
-			// utc date+time
1188
-			$utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"]) * 60) : $utcstart;
1189
-			$utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart;
1190
-
1191
-			$propsToSet = [];
1192
-			// update reminder time
1193
-			$propsToSet[$this->proptags["reminder_time"]] = $utcfirstoccstartdatetime;
1194
-
1195
-			// update first occurrence date
1196
-			$propsToSet[$this->proptags["startdate"]] = $propsToSet[$this->proptags["commonstart"]] = $utcfirstoccstartdatetime;
1197
-			$propsToSet[$this->proptags["duedate"]] = $propsToSet[$this->proptags["commonend"]] = $utcfirstoccenddatetime;
1198
-
1199
-			// Set Outlook properties, if it is an appointment
1200
-			if (isset($this->messageprops[$this->proptags["message_class"]]) && $this->messageprops[$this->proptags["message_class"]] == "IPM.Appointment") {
1201
-				// update real begin and real end date
1202
-				$propsToSet[$this->proptags["startdate_recurring"]] = $utcstart;
1203
-				$propsToSet[$this->proptags["enddate_recurring"]] = $utcend;
1204
-
1205
-				// recurrencetype
1206
-				// Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype
1207
-				$propsToSet[$this->proptags["recurrencetype"]] = ((int) $this->recur["type"]) - 0x9;
1208
-
1209
-				// set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting
1210
-				$propsToSet[$this->proptags["side_effects"]] = 369;
1211
-			}
1212
-			else {
1213
-				$propsToSet[$this->proptags["side_effects"]] = 3441;
1214
-			}
1215
-
1216
-			// FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog
1217
-			// Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset
1218
-			// to the 'next' occurrence; this makes sure that deleting the next occurrence will correctly set the reminder to
1219
-			// the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time)
1220
-			// with the reminder flag set.
1221
-			$reminderprops = mapi_getprops($this->message, [$this->proptags["reminder_minutes"]]);
1222
-			if (isset($reminderprops[$this->proptags["reminder_minutes"]])) {
1223
-				$occ = false;
1224
-				$occurrences = $this->getItems(time(), 0x7FF00000, 3, true);
1225
-
1226
-				for ($i = 0, $len = count($occurrences); $i < $len; ++$i) {
1227
-					// This will actually also give us appointments that have already started, but not yet ended. Since we want the next
1228
-					// reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum
1229
-					// number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case:
1230
-					// time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item
1231
-					// can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped.
1232
-
1233
-					if (($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) {
1234
-						$occ = $occurrences[$i];
1235
-
1236
-						break;
1237
-					}
1238
-				}
1239
-
1240
-				if ($occ) {
1241
-					$propsToSet[$this->proptags["flagdueby"]] = $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60);
1242
-				}
1243
-				else {
1244
-					// Last reminder passed, no reminders any more.
1245
-					$propsToSet[$this->proptags["reminder"]] = false;
1246
-					$propsToSet[$this->proptags["flagdueby"]] = 0x7FF00000;
1247
-				}
1248
-			}
1249
-
1250
-			// Default data
1251
-			// Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information)
1252
-			$rdata .= pack("VCCCC", 0x00003006, 0x08, 0x30, 0x00, 0x00);
1253
-
1254
-			if (isset($this->recur["startocc"], $this->recur["endocc"])) {
1255
-				// Set start and endtime in minutes
1256
-				$rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]);
1257
-			}
1258
-
1259
-			// Detailed exception data
1260
-
1261
-			$changed_items = $this->recur["changed_occurrences"];
1262
-
1263
-			$rdata .= pack("v", count($changed_items));
1264
-
1265
-			foreach ($changed_items as $changed_item) {
1266
-				// Set start and end time of exception
1267
-				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1268
-				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1269
-				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1270
-
1271
-				// Bitmask
1272
-				$bitmask = 0;
1273
-
1274
-				// Check for changed strings
1275
-				if (isset($changed_item["subject"])) {
1276
-					$bitmask |= 1 << 0;
1277
-				}
1278
-
1279
-				if (isset($changed_item["remind_before"])) {
1280
-					$bitmask |= 1 << 2;
1281
-				}
1282
-
1283
-				if (isset($changed_item["reminder_set"])) {
1284
-					$bitmask |= 1 << 3;
1285
-				}
1286
-
1287
-				if (isset($changed_item["location"])) {
1288
-					$bitmask |= 1 << 4;
1289
-				}
1290
-
1291
-				if (isset($changed_item["busystatus"])) {
1292
-					$bitmask |= 1 << 5;
1293
-				}
1294
-
1295
-				if (isset($changed_item["alldayevent"])) {
1296
-					$bitmask |= 1 << 7;
1297
-				}
1298
-
1299
-				if (isset($changed_item["label"])) {
1300
-					$bitmask |= 1 << 8;
1301
-				}
1302
-
1303
-				$rdata .= pack("v", $bitmask);
1304
-
1305
-				// Set "subject"
1306
-				if (isset($changed_item["subject"])) {
1307
-					// convert utf-8 to non-unicode blob string (us-ascii?)
1308
-					$subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]);
1309
-					$length = strlen($subject);
1310
-					$rdata .= pack("vv", $length + 1, $length);
1311
-					$rdata .= pack("a" . $length, $subject);
1312
-				}
1313
-
1314
-				if (isset($changed_item["remind_before"])) {
1315
-					$rdata .= pack("V", $changed_item["remind_before"]);
1316
-				}
1317
-
1318
-				if (isset($changed_item["reminder_set"])) {
1319
-					$rdata .= pack("V", $changed_item["reminder_set"]);
1320
-				}
1321
-
1322
-				if (isset($changed_item["location"])) {
1323
-					$location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]);
1324
-					$length = strlen($location);
1325
-					$rdata .= pack("vv", $length + 1, $length);
1326
-					$rdata .= pack("a" . $length, $location);
1327
-				}
1328
-
1329
-				if (isset($changed_item["busystatus"])) {
1330
-					$rdata .= pack("V", $changed_item["busystatus"]);
1331
-				}
1332
-
1333
-				if (isset($changed_item["alldayevent"])) {
1334
-					$rdata .= pack("V", $changed_item["alldayevent"]);
1335
-				}
1336
-
1337
-				if (isset($changed_item["label"])) {
1338
-					$rdata .= pack("V", $changed_item["label"]);
1339
-				}
1340
-			}
1341
-
1342
-			$rdata .= pack("V", 0);
1343
-
1344
-			// write extended data
1345
-			foreach ($changed_items as $changed_item) {
1346
-				$rdata .= pack("V", 0);
1347
-				if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1348
-					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1349
-					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1350
-					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1351
-				}
1352
-
1353
-				if (isset($changed_item["subject"])) {
1354
-					$subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]);
1355
-					$length = iconv_strlen($subject, "UCS-2LE");
1356
-					$rdata .= pack("v", $length);
1357
-					$rdata .= pack("a" . $length * 2, $subject);
1358
-				}
1359
-
1360
-				if (isset($changed_item["location"])) {
1361
-					$location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]);
1362
-					$length = iconv_strlen($location, "UCS-2LE");
1363
-					$rdata .= pack("v", $length);
1364
-					$rdata .= pack("a" . $length * 2, $location);
1365
-				}
1366
-
1367
-				if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1368
-					$rdata .= pack("V", 0);
1369
-				}
1370
-			}
1371
-
1372
-			$rdata .= pack("V", 0);
1373
-
1374
-			// Set props
1375
-			$propsToSet[$this->proptags["recurring_data"]] = $rdata;
1376
-			$propsToSet[$this->proptags["recurring"]] = true;
1377
-
1378
-			if (isset($this->tz) && $this->tz) {
1379
-				$timezone = "GMT";
1380
-				if ($this->tz["timezone"] != 0) {
1381
-					// Create user readable timezone information
1382
-					$timezone = sprintf(
1383
-						"(GMT %s%02d:%02d)",
1384
-						(-$this->tz["timezone"] > 0 ? "+" : "-"),
1385
-						abs($this->tz["timezone"] / 60),
1386
-						abs($this->tz["timezone"] % 60)
1387
-					);
1388
-				}
1389
-				$propsToSet[$this->proptags["timezone_data"]] = $this->getTimezoneData($this->tz);
1390
-				$propsToSet[$this->proptags["timezone"]] = $timezone;
1391
-			}
1392
-			mapi_setprops($this->message, $propsToSet);
1393
-		}
1394
-
1395
-		/**
1396
-		 * Function which converts a recurrence date timestamp to an unix date timestamp.
1397
-		 *
1398
-		 * @author Steve Hardy
1399
-		 *
1400
-		 * @param int $rdate the date which will be converted
1401
-		 *
1402
-		 * @return int the converted date
1403
-		 */
1404
-		public function recurDataToUnixData($rdate) {
1405
-			return ($rdate - 194074560) * 60;
1406
-		}
1407
-
1408
-		/**
1409
-		 * Function which converts an unix date timestamp to recurrence date timestamp.
1410
-		 *
1411
-		 * @author Johnny Biemans
1412
-		 *
1413
-		 * @param Date $date the date which will be converted
1414
-		 *
1415
-		 * @return int the converted date in minutes
1416
-		 */
1417
-		public function unixDataToRecurData($date) {
1418
-			return ($date / 60) + 194074560;
1419
-		}
1420
-
1421
-		/**
1422
-		 * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1423
-		 *
1424
-		 * @author Steve Hardy
1425
-		 *
1426
-		 * @param mixed $ts
1427
-		 */
1428
-		public function GetTZOffset($ts) {
1429
-			$Offset = date("O", $ts);
1430
-
1431
-			$Parity = $Offset < 0 ? -1 : 1;
1432
-			$Offset = $Parity * $Offset;
1433
-			$Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
1434
-
1435
-			return $Parity * $Offset;
1436
-		}
1437
-
1438
-		/**
1439
-		 * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1440
-		 *
1441
-		 * @author Steve Hardy
1442
-		 *
1443
-		 * @param Date $time
1444
-		 *
1445
-		 * @return Date GMT Time
1446
-		 */
1447
-		public function gmtime($time) {
1448
-			$TZOffset = $this->GetTZOffset($time);
1449
-
1450
-			$t_time = $time - $TZOffset * 60; # Counter adjust for localtime()
1451
-
1452
-			return localtime($t_time, 1);
1453
-		}
1454
-
1455
-		public function isLeapYear($year) {
1456
-			return $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0);
1457
-		}
1458
-
1459
-		public function getMonthInSeconds($year, $month) {
1460
-			if (in_array($month, [1, 3, 5, 7, 8, 10, 12])) {
1461
-				$day = 31;
1462
-			}
1463
-			elseif (in_array($month, [4, 6, 9, 11])) {
1464
-				$day = 30;
1465
-			}
1466
-			else {
1467
-				$day = 28;
1468
-				if ($this->isLeapYear($year) == 1) {
1469
-					++$day;
1470
-				}
1471
-			}
1472
-
1473
-			return $day * 24 * 60 * 60;
1474
-		}
1475
-
1476
-		/**
1477
-		 * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour.
1478
-		 *
1479
-		 * @param int $year
1480
-		 * @param int $month
1481
-		 * @param int $week
1482
-		 * @param int $day
1483
-		 * @param int $hour
1484
-		 *
1485
-		 * @return returns the timestamp of the given date, timezone-independant
1486
-		 */
1487
-		public function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour) {
1488
-			// get first day of month
1489
-			$date = gmmktime(0, 0, 0, $month, 0, $year + 1900);
1490
-
1491
-			// get wday info
1492
-			$gmdate = $this->gmtime($date);
1493
-
1494
-			$date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week
1495
-
1496
-			$date += $week * 7 * 24 * 60 * 60; // go to correct week nr
1497
-			$date += $day * 24 * 60 * 60;
1498
-			$date += $hour * 60 * 60;
1499
-
1500
-			$gmdate = $this->gmtime($date);
1501
-
1502
-			// if we are in the next month, then back up a week, because week '5' means
1503
-			// 'last week of month'
1504
-
1505
-			if ($month != $gmdate["tm_mon"] + 1) {
1506
-				$date -= 7 * 24 * 60 * 60;
1507
-			}
1508
-
1509
-			return $date;
1510
-		}
1511
-
1512
-		/**
1513
-		 * getTimezone gives the timezone offset (in minutes) of the given
1514
-		 * local date/time according to the given TZ info.
1515
-		 *
1516
-		 * @param mixed $tz
1517
-		 * @param mixed $date
1518
-		 */
1519
-		public function getTimezone($tz, $date) {
1520
-			// No timezone -> GMT (+0)
1521
-			if (!isset($tz["timezone"])) {
1522
-				return 0;
1523
-			}
1524
-
1525
-			$dst = false;
1526
-			$gmdate = $this->gmtime($date);
1527
-
1528
-			$dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]);
1529
-			$dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]);
1530
-
1531
-			if ($dststart <= $dstend) {
1532
-				// Northern hemisphere, eg DST is during Mar-Oct
1533
-				if ($date > $dststart && $date < $dstend) {
1534
-					$dst = true;
1535
-				}
1536
-			}
1537
-			else {
1538
-				// Southern hemisphere, eg DST is during Oct-Mar
1539
-				if ($date < $dstend || $date > $dststart) {
1540
-					$dst = true;
1541
-				}
1542
-			}
1543
-
1544
-			if ($dst) {
1545
-				return $tz["timezone"] + $tz["timezonedst"];
1546
-			}
1547
-
1548
-			return $tz["timezone"];
1549
-		}
1550
-
1551
-		/**
1552
-		 * getWeekNr() returns the week nr of the month (ie first week of february is 1).
1553
-		 *
1554
-		 * @param mixed $date
1555
-		 */
1556
-		public function getWeekNr($date) {
1557
-			$gmdate = gmtime($date);
1558
-			$gmdate["tm_mday"] = 0;
1559
-
1560
-			return strftime("%W", $date) - strftime("%W", gmmktime($gmdate)) + 1;
1561
-		}
1562
-
1563
-		/**
1564
-		 * parseTimezone parses the timezone as specified in named property 0x8233
1565
-		 * in Outlook calendar messages. Returns the timezone in minutes negative
1566
-		 * offset (GMT +2:00 -> -120).
1567
-		 *
1568
-		 * @param mixed $data
1569
-		 */
1570
-		public function parseTimezone($data) {
1571
-			if (strlen($data) < 48) {
1572
-				return;
1573
-			}
1574
-
1575
-			return unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data);
1576
-		}
1577
-
1578
-		public function getTimezoneData($tz) {
1579
-			return pack("lllllvvllvlvvlv", $tz["timezone"], 0, $tz["timezonedst"], 0, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendhour"], 0, 0, 0, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststarthour"], 0, 0);
1580
-		}
1581
-
1582
-		/**
1583
-		 * createTimezone creates the timezone as specified in the named property 0x8233
1584
-		 * see also parseTimezone()
1585
-		 * $tz is an array with the timezone data.
1586
-		 *
1587
-		 * @param mixed $tz
1588
-		 */
1589
-		public function createTimezone($tz) {
1590
-			return pack(
1591
-				"lxxxxlxxxxlvvxxxxxxxxxxlvvxxxxxx",
1592
-				$tz["timezone"],
1593
-				array_key_exists("timezonedst", $tz) ? $tz["timezonedst"] : 0,
1594
-				array_key_exists("dstendmonth", $tz) ? $tz["dstendmonth"] : 0,
1595
-				array_key_exists("dstendweek", $tz) ? $tz["dstendweek"] : 0,
1596
-				array_key_exists("dstendhour", $tz) ? $tz["dstendhour"] : 0,
1597
-				array_key_exists("dststartmonth", $tz) ? $tz["dststartmonth"] : 0,
1598
-				array_key_exists("dststartweek", $tz) ? $tz["dststartweek"] : 0,
1599
-				array_key_exists("dststarthour", $tz) ? $tz["dststarthour"] : 0
1600
-			);
1601
-		}
1602
-
1603
-		/**
1604
-		 * toGMT returns a timestamp in GMT time for the time and timezone given.
1605
-		 *
1606
-		 * @param mixed $tz
1607
-		 * @param mixed $date
1608
-		 */
1609
-		public function toGMT($tz, $date) {
1610
-			if (!isset($tz['timezone'])) {
1611
-				return $date;
1612
-			}
1613
-			$offset = $this->getTimezone($tz, $date);
1614
-
1615
-			return $date + $offset * 60;
1616
-		}
1617
-
1618
-		/**
1619
-		 * fromGMT returns a timestamp in the local timezone given from the GMT time given.
1620
-		 *
1621
-		 * @param mixed $tz
1622
-		 * @param mixed $date
1623
-		 */
1624
-		public function fromGMT($tz, $date) {
1625
-			$offset = $this->getTimezone($tz, $date);
1626
-
1627
-			return $date - $offset * 60;
1628
-		}
1629
-
1630
-		/**
1631
-		 * Function to get timestamp of the beginning of the day of the timestamp given.
1632
-		 *
1633
-		 * @param date $date
1634
-		 *
1635
-		 * @return date timestamp referring to same day but at 00:00:00
1636
-		 */
1637
-		public function dayStartOf($date) {
1638
-			$time1 = $this->gmtime($date);
1639
-
1640
-			return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900);
1641
-		}
1642
-
1643
-		/**
1644
-		 * Function to get timestamp of the beginning of the month of the timestamp given.
1645
-		 *
1646
-		 * @param date $date
1647
-		 *
1648
-		 * @return date Timestamp referring to same month but on the first day, and at 00:00:00
1649
-		 */
1650
-		public function monthStartOf($date) {
1651
-			$time1 = $this->gmtime($date);
1652
-
1653
-			return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900);
1654
-		}
1655
-
1656
-		/**
1657
-		 * Function to get timestamp of the beginning of the year of the timestamp given.
1658
-		 *
1659
-		 * @param date $date
1660
-		 *
1661
-		 * @return date Timestamp referring to the same year but on Jan 01, at 00:00:00
1662
-		 */
1663
-		public function yearStartOf($date) {
1664
-			$time1 = $this->gmtime($date);
1665
-
1666
-			return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900);
1667
-		}
1668
-
1669
-		/**
1670
-		 * Function which returns the items in a given interval. This included expansion of the recurrence and
1671
-		 * processing of exceptions (modified and deleted).
1672
-		 *
1673
-		 * @param string $entryid       the entryid of the message
1674
-		 * @param array  $props         the properties of the message
1675
-		 * @param date   $start         start time of the interval (GMT)
1676
-		 * @param date   $end           end time of the interval (GMT)
1677
-		 * @param mixed  $limit
1678
-		 * @param mixed  $remindersonly
1679
-		 */
1680
-		public function getItems($start, $end, $limit = 0, $remindersonly = false) {
1681
-			$items = [];
1682
-
1683
-			if (isset($this->recur)) {
1684
-				// Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which
1685
-				// exceptions are in range and have a reminder set
1686
-				if ($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) {
1687
-					// Sort exceptions by start time
1688
-					uasort($this->recur["changed_occurrences"], [$this, "sortExceptionStart"]);
1689
-
1690
-					// Loop through all changed exceptions
1691
-					foreach ($this->recur["changed_occurrences"] as $exception) {
1692
-						// Check reminder set
1693
-						if (!isset($exception["reminder"]) || $exception["reminder"] == false) {
1694
-							continue;
1695
-						}
1696
-
1697
-						// Convert to GMT
1698
-						$occstart = $this->toGMT($this->tz, $exception["start"]); // seb changed $tz to $this->tz
1699
-						$occend = $this->toGMT($this->tz, $exception["end"]); // seb changed $tz to $this->tz
1700
-
1701
-						// Check range criterium
1702
-						if ($occstart > $end || $occend < $start) {
1703
-							continue;
1704
-						}
1705
-
1706
-						// OK, add to items.
1707
-						array_push($items, $this->getExceptionProperties($exception));
1708
-						if ($limit && (count($items) == $limit)) {
1709
-							break;
1710
-						}
1711
-					}
1712
-
1713
-					uasort($items, [$this, "sortStarttime"]);
1714
-
1715
-					return $items;
1716
-				}
1717
-
1718
-				// From here on, the dates of the occurrences are calculated in local time, so the days we're looking
1719
-				// at are calculated from the local time dates of $start and $end
1720
-				// TODO use one isset
1721
-				if (isset($this->recur['regen'], $this->action['datecompleted']) && $this->recur['regen']) {
1722
-					$daystart = $this->dayStartOf($this->action['datecompleted']);
1723
-				}
1724
-				else {
1725
-					$daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence
1726
-				}
1727
-
1728
-				// Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view
1729
-				// or the end of the recurrence, whichever comes first
1730
-				if ($end > $this->toGMT($this->tz, $this->recur["end"])) {
1731
-					$rangeend = $this->toGMT($this->tz, $this->recur["end"]);
1732
-				}
1733
-				else {
1734
-					$rangeend = $end;
1735
-				}
1736
-
1737
-				$dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend));
1738
-
1739
-				// Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range.
1740
-
1741
-				switch ($this->recur["type"]) {
1742
-				case 10:
1743
-					// Daily
1744
-					if ($this->recur["everyn"] <= 0) {
1745
-						$this->recur["everyn"] = 1440;
1746
-					}
1747
-
1748
-					if ($this->recur["subtype"] == 0) {
1749
-						// Every Nth day
1750
-						for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) {
1751
-							$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1752
-						}
1753
-					}
1754
-					else {
1755
-						// Every workday
1756
-						for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440) {
1757
-							$nowtime = $this->gmtime($now);
1758
-							if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace
1759
-								$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1760
-							}
1761
-						}
1762
-					}
1763
-
1764
-					break;
1765
-
1766
-				case 11:
1767
-					// Weekly
1768
-					if ($this->recur["everyn"] <= 0) {
1769
-						$this->recur["everyn"] = 1;
1770
-					}
1771
-
1772
-					// If sliding flag is set then move to 'n' weeks
1773
-					if ($this->recur['regen']) {
1774
-						$daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]);
1775
-					}
1776
-
1777
-					for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"])) {
1778
-						if ($this->recur['regen']) {
1779
-							$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1780
-						}
1781
-						else {
1782
-							// Loop through the whole following week to the first occurrence of the week, add each day that is specified
1783
-							for ($wday = 0; $wday < 7; ++$wday) {
1784
-								$daynow = $now + $wday * 60 * 60 * 24;
1785
-								// checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item
1786
-								if ($daynow <= $dayend) {
1787
-									$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1788
-									if (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"]))) { // Selected ?
1789
-										$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1790
-									}
1791
-								}
1792
-							}
1793
-						}
1794
-					}
1795
-
1796
-					break;
1797
-
1798
-				case 12:
1799
-					// Monthly
1800
-					if ($this->recur["everyn"] <= 0) {
1801
-						$this->recur["everyn"] = 1;
1802
-					}
1803
-
1804
-					// Loop through all months from start to end of occurrence, starting at beginning of first month
1805
-					for ($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
1806
-						if (isset($this->recur["monthday"]) && ($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months
1807
-							$difference = 1;
1808
-							if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) {
1809
-								$difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1;
1810
-							}
1811
-							$daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60);
1812
-							// checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item
1813
-							if ($daynow <= $dayend) {
1814
-								$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1815
-							}
1816
-						}
1817
-						elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] of every N months
1818
-							// Sanitize input
1819
-							if ($this->recur["weekdays"] == 0) {
1820
-								$this->recur["weekdays"] = 1;
1821
-							}
1822
-
1823
-							// If nday is not set to the last day in the month
1824
-							if ($this->recur["nday"] < 5) {
1825
-								// keep the track of no. of time correct selection pattern(like 2nd weekday, 4th fiday, etc.)is matched
1826
-								$ndaycounter = 0;
1827
-								// Find matching weekday in this month
1828
-								for ($day = 0, $total = $this->daysInMonth($now, 1); $day < $total; ++$day) {
1829
-									$daynow = $now + $day * 60 * 60 * 24;
1830
-									$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1831
-
1832
-									if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1833
-										++$ndaycounter;
1834
-									}
1835
-									// check the selected pattern is same as asked Nth weekday,If so set the firstday
1836
-									if ($this->recur["nday"] == $ndaycounter) {
1837
-										$firstday = $day;
1838
-
1839
-										break;
1840
-									}
1841
-								}
1842
-								// $firstday is the day of the month on which the asked pattern of nth weekday matches
1843
-								$daynow = $now + $firstday * 60 * 60 * 24;
1844
-							}
1845
-							else {
1846
-								// Find last day in the month ($now is the firstday of the month)
1847
-								$NumDaysInMonth = $this->daysInMonth($now, 1);
1848
-								$daynow = $now + (($NumDaysInMonth - 1) * 24 * 60 * 60);
1849
-
1850
-								$nowtime = $this->gmtime($daynow);
1851
-								while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) == 0) {
1852
-									$daynow -= 86400;
1853
-									$nowtime = $this->gmtime($daynow);
1854
-								}
1855
-							}
1856
-
1857
-							/*
8
+    /**
9
+     * BaseRecurrence
10
+     * this class is superclass for recurrence for appointments and tasks. This class provides all
11
+     * basic features of recurrence.
12
+     */
13
+    class BaseRecurrence {
14
+        /**
15
+         * @var object Mapi Message Store (may be null if readonly)
16
+         */
17
+        public $store;
18
+
19
+        /**
20
+         * @var object Mapi Message (may be null if readonly)
21
+         */
22
+        public $message;
23
+
24
+        /**
25
+         * @var array Message Properties
26
+         */
27
+        public $messageprops;
28
+
29
+        /**
30
+         * @var array list of property tags
31
+         */
32
+        public $proptags;
33
+
34
+        /**
35
+         * @var recurrence data of this calendar item
36
+         */
37
+        public $recur;
38
+
39
+        /**
40
+         * @var Timezone data of this calendar item
41
+         */
42
+        public $tz;
43
+
44
+        /**
45
+         * Constructor.
46
+         *
47
+         * @param resource $store      MAPI Message Store Object
48
+         * @param resource $message    the MAPI (appointment) message
49
+         * @param array    $properties the list of MAPI properties the message has
50
+         */
51
+        public function __construct($store, $message) {
52
+            $this->store = $store;
53
+
54
+            if (is_array($message)) {
55
+                $this->messageprops = $message;
56
+            }
57
+            else {
58
+                $this->message = $message;
59
+                $this->messageprops = mapi_getprops($this->message, $this->proptags);
60
+            }
61
+
62
+            if (isset($this->messageprops[$this->proptags["recurring_data"]])) {
63
+                // There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface
64
+                if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255) {
65
+                    $this->getFullRecurrenceBlob();
66
+                }
67
+
68
+                $this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]);
69
+            }
70
+            if (isset($this->proptags["timezone_data"], $this->messageprops[$this->proptags["timezone_data"]])) {
71
+                $this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]);
72
+            }
73
+        }
74
+
75
+        public function getRecurrence() {
76
+            return $this->recur;
77
+        }
78
+
79
+        public function getFullRecurrenceBlob() {
80
+            $message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]);
81
+
82
+            $recurrBlob = '';
83
+            $stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
84
+            $stat = mapi_stream_stat($stream);
85
+
86
+            for ($i = 0; $i < $stat['cb']; $i += 1024) {
87
+                $recurrBlob .= mapi_stream_read($stream, 1024);
88
+            }
89
+
90
+            if (!empty($recurrBlob)) {
91
+                $this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob;
92
+            }
93
+        }
94
+
95
+        /**
96
+         * Function for parsing the Recurrence value of a Calendar item.
97
+         *
98
+         * Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the
99
+         * data to this function
100
+         *
101
+         * Returns a structure containing the data:
102
+         *
103
+         * type        - type of recurrence: day=10, week=11, month=12, year=13
104
+         * subtype    - type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday)
105
+         * start    - unix timestamp of first occurrence
106
+         * end        - unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1
107
+         * numoccur     - occurrences (may be very large when there is no end data)
108
+         *
109
+         * then, for each type:
110
+         *
111
+         * Daily:
112
+         *  everyn    - every [everyn] days in minutes
113
+         *  regen    - regenerating event (like tasks)
114
+         *
115
+         * Weekly:
116
+         *  everyn    - every [everyn] weeks in weeks
117
+         *  regen    - regenerating event (like tasks)
118
+         *  weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
119
+         *
120
+         * Monthly:
121
+         *  everyn    - every [everyn] months
122
+         *  regen    - regenerating event (like tasks)
123
+         *
124
+         *  subtype 2:
125
+         *      monthday - on day [monthday] of the month
126
+         *
127
+         *  subtype 3:
128
+         *      weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
129
+         *   nday    - on [nday]'th [weekdays] of the month
130
+         *
131
+         * Yearly:
132
+         *  everyn    - every [everyn] months (12, 24, 36, ...)
133
+         *  month    - in month [month] (although the month is encoded in minutes since the startning of the year ........)
134
+         *  regen    - regenerating event (like tasks)
135
+         *
136
+         *  subtype 2:
137
+         *   monthday - on day [monthday] of the month
138
+         *
139
+         *  subtype 3:
140
+         *   weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
141
+         *      nday    - on [nday]'th [weekdays] of the month [month]
142
+         *
143
+         * @param string $rdata Binary string
144
+         *
145
+         * @return array recurrence data
146
+         */
147
+        public function parseRecurrence($rdata) {
148
+            if (strlen($rdata) < 10) {
149
+                return;
150
+            }
151
+
152
+            $ret["changed_occurrences"] = [];
153
+            $ret["deleted_occurrences"] = [];
154
+
155
+            $data = unpack("Vconst1/Crtype/Cconst2/Vrtype2", $rdata);
156
+
157
+            $ret["type"] = $data["rtype"];
158
+            $ret["subtype"] = $data["rtype2"];
159
+            $rdata = substr($rdata, 10);
160
+
161
+            switch ($data["rtype"]) {
162
+                case 0x0A:
163
+                    // Daily
164
+                    if (strlen($rdata) < 12) {
165
+                        return $ret;
166
+                    }
167
+
168
+                    $data = unpack("Vunknown/Veveryn/Vregen", $rdata);
169
+                    $ret["everyn"] = $data["everyn"];
170
+                    $ret["regen"] = $data["regen"];
171
+
172
+                    switch ($ret["subtype"]) {
173
+                        case 0:
174
+                            $rdata = substr($rdata, 12);
175
+
176
+                            break;
177
+
178
+                        case 1:
179
+                            $rdata = substr($rdata, 16);
180
+
181
+                            break;
182
+                    }
183
+
184
+                    break;
185
+
186
+                case 0x0B:
187
+                    // Weekly
188
+                    if (strlen($rdata) < 16) {
189
+                        return $ret;
190
+                    }
191
+
192
+                    $data = unpack("Vconst1/Veveryn/Vregen", $rdata);
193
+                    $rdata = substr($rdata, 12);
194
+
195
+                    $ret["everyn"] = $data["everyn"];
196
+                    $ret["regen"] = $data["regen"];
197
+                    $ret["weekdays"] = 0;
198
+
199
+                    if ($data["regen"] == 0) {
200
+                        $data = unpack("Vweekdays", $rdata);
201
+                        $rdata = substr($rdata, 4);
202
+
203
+                        $ret["weekdays"] = $data["weekdays"];
204
+                    }
205
+
206
+                    break;
207
+
208
+                case 0x0C:
209
+                    // Monthly
210
+                    if (strlen($rdata) < 16) {
211
+                        return $ret;
212
+                    }
213
+
214
+                    $data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata);
215
+
216
+                    $ret["everyn"] = $data["everyn"];
217
+                    $ret["regen"] = $data["regen"];
218
+
219
+                    if ($ret["subtype"] == 3) {
220
+                        $ret["weekdays"] = $data["monthday"];
221
+                    }
222
+                    else {
223
+                        $ret["monthday"] = $data["monthday"];
224
+                    }
225
+
226
+                    $rdata = substr($rdata, 16);
227
+
228
+                    if ($ret["subtype"] == 3) {
229
+                        $data = unpack("Vnday", $rdata);
230
+                        $ret["nday"] = $data["nday"];
231
+                        $rdata = substr($rdata, 4);
232
+                    }
233
+
234
+                    break;
235
+
236
+                case 0x0D:
237
+                    // Yearly
238
+                    if (strlen($rdata) < 16) {
239
+                        return $ret;
240
+                    }
241
+
242
+                    $data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata);
243
+
244
+                    $ret["month"] = $data["month"];
245
+                    $ret["everyn"] = $data["everyn"];
246
+                    $ret["regen"] = $data["regen"];
247
+
248
+                    if ($ret["subtype"] == 3) {
249
+                        $ret["weekdays"] = $data["monthday"];
250
+                    }
251
+                    else {
252
+                        $ret["monthday"] = $data["monthday"];
253
+                    }
254
+
255
+                    $rdata = substr($rdata, 16);
256
+
257
+                    if ($ret["subtype"] == 3) {
258
+                        $data = unpack("Vnday", $rdata);
259
+                        $ret["nday"] = $data["nday"];
260
+                        $rdata = substr($rdata, 4);
261
+                    }
262
+
263
+                    break;
264
+            }
265
+
266
+            if (strlen($rdata) < 16) {
267
+                return $ret;
268
+            }
269
+
270
+            $data = unpack("Cterm/C3const1/Vnumoccur/Vconst2/Vnumexcept", $rdata);
271
+
272
+            $rdata = substr($rdata, 16);
273
+
274
+            $ret["term"] = $data["term"];
275
+            $ret["numoccur"] = $data["numoccur"];
276
+            $ret["numexcept"] = $data["numexcept"];
277
+
278
+            // exc_base_dates are *all* the base dates that have been either deleted or modified
279
+            $exc_base_dates = [];
280
+            for ($i = 0; $i < $ret["numexcept"]; ++$i) {
281
+                if (strlen($rdata) < 4) {
282
+                    // We shouldn't arrive here, because that implies
283
+                    // numexcept does not match the amount of data
284
+                    // which is available for the exceptions.
285
+                    return $ret;
286
+                }
287
+                $data = unpack("Vbasedate", $rdata);
288
+                $rdata = substr($rdata, 4);
289
+                $exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]);
290
+            }
291
+
292
+            if (strlen($rdata) < 4) {
293
+                return $ret;
294
+            }
295
+
296
+            $data = unpack("Vnumexceptmod", $rdata);
297
+            $rdata = substr($rdata, 4);
298
+
299
+            $ret["numexceptmod"] = $data["numexceptmod"];
300
+
301
+            // exc_changed are the base dates of *modified* occurrences. exactly what is modified
302
+            // is in the attachments *and* in the data further down this function.
303
+            $exc_changed = [];
304
+            for ($i = 0; $i < $ret["numexceptmod"]; ++$i) {
305
+                if (strlen($rdata) < 4) {
306
+                    // We shouldn't arrive here, because that implies
307
+                    // numexceptmod does not match the amount of data
308
+                    // which is available for the exceptions.
309
+                    return $ret;
310
+                }
311
+                $data = unpack("Vstartdate", $rdata);
312
+                $rdata = substr($rdata, 4);
313
+                $exc_changed[] = $this->recurDataToUnixData($data["startdate"]);
314
+            }
315
+
316
+            if (strlen($rdata) < 8) {
317
+                return $ret;
318
+            }
319
+
320
+            $data = unpack("Vstart/Vend", $rdata);
321
+            $rdata = substr($rdata, 8);
322
+
323
+            $ret["start"] = $this->recurDataToUnixData($data["start"]);
324
+            $ret["end"] = $this->recurDataToUnixData($data["end"]);
325
+
326
+            // this is where task recurrence stop
327
+            if (strlen($rdata) < 16) {
328
+                return $ret;
329
+            }
330
+
331
+            $data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata);
332
+            $rdata = substr($rdata, 16);
333
+
334
+            $ret["startocc"] = $data["startmin"];
335
+            $ret["endocc"] = $data["endmin"];
336
+            $writerversion = $data["writerversion"];
337
+
338
+            $data = unpack("vnumber", $rdata);
339
+            $rdata = substr($rdata, 2);
340
+
341
+            $nexceptions = $data["number"];
342
+            $exc_changed_details = [];
343
+
344
+            // Parse n modified exceptions
345
+            for ($i = 0; $i < $nexceptions; ++$i) {
346
+                $item = [];
347
+
348
+                // Get exception startdate, enddate and basedate (the date at which the occurrence would have started)
349
+                $data = unpack("Vstartdate/Venddate/Vbasedate", $rdata);
350
+                $rdata = substr($rdata, 12);
351
+
352
+                // Convert recurtimestamp to unix timestamp
353
+                $startdate = $this->recurDataToUnixData($data["startdate"]);
354
+                $enddate = $this->recurDataToUnixData($data["enddate"]);
355
+                $basedate = $this->recurDataToUnixData($data["basedate"]);
356
+
357
+                // Set the right properties
358
+                $item["basedate"] = $this->dayStartOf($basedate);
359
+                $item["start"] = $startdate;
360
+                $item["end"] = $enddate;
361
+
362
+                $data = unpack("vbitmask", $rdata);
363
+                $rdata = substr($rdata, 2);
364
+                $item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions
365
+
366
+                // Bitmask to verify what properties are changed
367
+                $bitmask = $data["bitmask"];
368
+
369
+                // ARO_SUBJECT: 0x0001
370
+                // Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject
371
+                if (($bitmask & (1 << 0))) {
372
+                    $data = unpack("vnull_length/vlength", $rdata);
373
+                    $rdata = substr($rdata, 4);
374
+
375
+                    $length = $data["length"];
376
+                    $item["subject"] = ""; // Normalized subject
377
+                    for ($j = 0; $j < $length && strlen($rdata); ++$j) {
378
+                        $data = unpack("Cchar", $rdata);
379
+                        $rdata = substr($rdata, 1);
380
+
381
+                        $item["subject"] .= chr($data["char"]);
382
+                    }
383
+                }
384
+
385
+                // ARO_MEETINGTYPE: 0x0002
386
+                if (($bitmask & (1 << 1))) {
387
+                    $rdata = substr($rdata, 4);
388
+                    // Attendees modified: no data here (only in attachment)
389
+                }
390
+
391
+                // ARO_REMINDERDELTA: 0x0004
392
+                // Look for field: ReminderDelta (4b)
393
+                if (($bitmask & (1 << 2))) {
394
+                    $data = unpack("Vremind_before", $rdata);
395
+                    $rdata = substr($rdata, 4);
396
+
397
+                    $item["remind_before"] = $data["remind_before"];
398
+                }
399
+
400
+                // ARO_REMINDER: 0x0008
401
+                // Look field: ReminderSet (4b)
402
+                if (($bitmask & (1 << 3))) {
403
+                    $data = unpack("Vreminder_set", $rdata);
404
+                    $rdata = substr($rdata, 4);
405
+
406
+                    $item["reminder_set"] = $data["reminder_set"];
407
+                }
408
+
409
+                // ARO_LOCATION: 0x0010
410
+                // Look for fields: LocationLength (2b), LocationLength2 (2b) and Location
411
+                // Similar to ARO_SUBJECT above.
412
+                if (($bitmask & (1 << 4))) {
413
+                    $data = unpack("vnull_length/vlength", $rdata);
414
+                    $rdata = substr($rdata, 4);
415
+
416
+                    $item["location"] = "";
417
+
418
+                    $length = $data["length"];
419
+                    $data = substr($rdata, 0, $length);
420
+                    $rdata = substr($rdata, $length);
421
+
422
+                    $item["location"] .= $data;
423
+                }
424
+
425
+                // ARO_BUSYSTATUS: 0x0020
426
+                // Look for field: BusyStatus (4b)
427
+                if (($bitmask & (1 << 5))) {
428
+                    $data = unpack("Vbusystatus", $rdata);
429
+                    $rdata = substr($rdata, 4);
430
+
431
+                    $item["busystatus"] = $data["busystatus"];
432
+                }
433
+
434
+                // ARO_ATTACHMENT: 0x0040
435
+                if (($bitmask & (1 << 6))) {
436
+                    // no data: RESERVED
437
+                    $rdata = substr($rdata, 4);
438
+                }
439
+
440
+                // ARO_SUBTYPE: 0x0080
441
+                // Look for field: SubType (4b). Determines whether it is an allday event.
442
+                if (($bitmask & (1 << 7))) {
443
+                    $data = unpack("Vallday", $rdata);
444
+                    $rdata = substr($rdata, 4);
445
+
446
+                    $item["alldayevent"] = $data["allday"];
447
+                }
448
+
449
+                // ARO_APPTCOLOR: 0x0100
450
+                // Look for field: AppointmentColor (4b)
451
+                if (($bitmask & (1 << 8))) {
452
+                    $data = unpack("Vlabel", $rdata);
453
+                    $rdata = substr($rdata, 4);
454
+
455
+                    $item["label"] = $data["label"];
456
+                }
457
+
458
+                // ARO_EXCEPTIONAL_BODY: 0x0200
459
+                if (($bitmask & (1 << 9))) {
460
+                    // Notes or Attachments modified: no data here (only in attachment)
461
+                }
462
+
463
+                array_push($exc_changed_details, $item);
464
+            }
465
+
466
+            /**
467
+             * We now have $exc_changed, $exc_base_dates and $exc_changed_details
468
+             * We will ignore $exc_changed, as this information is available in $exc_changed_details
469
+             * also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item
470
+             * has been deleted.
471
+             */
472
+
473
+            // Find deleted occurrences
474
+            $deleted_occurrences = [];
475
+
476
+            foreach ($exc_base_dates as $base_date) {
477
+                $found = false;
478
+
479
+                foreach ($exc_changed_details as $details) {
480
+                    if ($details["basedate"] == $base_date) {
481
+                        $found = true;
482
+
483
+                        break;
484
+                    }
485
+                }
486
+                if (!$found) {
487
+                    // item was not in exc_changed_details, so it must be deleted
488
+                    $deleted_occurrences[] = $base_date;
489
+                }
490
+            }
491
+
492
+            $ret["deleted_occurrences"] = $deleted_occurrences;
493
+            $ret["changed_occurrences"] = $exc_changed_details;
494
+
495
+            // enough data for normal exception (no extended data)
496
+            if (strlen($rdata) < 16) {
497
+                return $ret;
498
+            }
499
+
500
+            $data = unpack("Vreservedsize", $rdata);
501
+            $rdata = substr($rdata, 4 + $data["reservedsize"]);
502
+
503
+            for ($i = 0; $i < $nexceptions; ++$i) {
504
+                // subject and location in ucs-2 to utf-8
505
+                if ($writerversion >= 0x3009) {
506
+                    $data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4
507
+                    $rdata = substr($rdata, 4 + $data["size"]);
508
+                }
509
+
510
+                $data = unpack("Vreservedsize", $rdata);
511
+                $rdata = substr($rdata, 4 + $data["reservedsize"]);
512
+
513
+                // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
514
+                if ($exc_changed_details[$i]["bitmask"] & 0x11) {
515
+                    $data = unpack("Vstart/Vend/Vorig", $rdata);
516
+                    $rdata = substr($rdata, 4 * 3);
517
+
518
+                    $exc_changed_details[$i]["ex_start_datetime"] = $data["start"];
519
+                    $exc_changed_details[$i]["ex_end_datetime"] = $data["end"];
520
+                    $exc_changed_details[$i]["ex_orig_date"] = $data["orig"];
521
+                }
522
+
523
+                // ARO_SUBJECT
524
+                if ($exc_changed_details[$i]["bitmask"] & 0x01) {
525
+                    // decode ucs2 string to utf-8
526
+                    $data = unpack("vlength", $rdata);
527
+                    $rdata = substr($rdata, 2);
528
+                    $length = $data["length"];
529
+                    $data = substr($rdata, 0, $length * 2);
530
+                    $rdata = substr($rdata, $length * 2);
531
+                    $subject = iconv("UCS-2LE", "UTF-8", $data);
532
+                    // replace subject with unicode subject
533
+                    $exc_changed_details[$i]["subject"] = $subject;
534
+                }
535
+
536
+                // ARO_LOCATION
537
+                if ($exc_changed_details[$i]["bitmask"] & 0x10) {
538
+                    // decode ucs2 string to utf-8
539
+                    $data = unpack("vlength", $rdata);
540
+                    $rdata = substr($rdata, 2);
541
+                    $length = $data["length"];
542
+                    $data = substr($rdata, 0, $length * 2);
543
+                    $rdata = substr($rdata, $length * 2);
544
+                    $location = iconv("UCS-2LE", "UTF-8", $data);
545
+                    // replace subject with unicode subject
546
+                    $exc_changed_details[$i]["location"] = $location;
547
+                }
548
+
549
+                // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
550
+                if ($exc_changed_details[$i]["bitmask"] & 0x11) {
551
+                    $data = unpack("Vreservedsize", $rdata);
552
+                    $rdata = substr($rdata, 4 + $data["reservedsize"]);
553
+                }
554
+            }
555
+
556
+            // update with extended data
557
+            $ret["changed_occurrences"] = $exc_changed_details;
558
+
559
+            return $ret;
560
+        }
561
+
562
+        /**
563
+         * Saves the recurrence data to the recurrence property.
564
+         */
565
+        public function saveRecurrence() {
566
+            // Only save if a message was passed
567
+            if (!isset($this->message)) {
568
+                return;
569
+            }
570
+
571
+            // Abort if no recurrence was set
572
+            if (!isset($this->recur["type"], $this->recur["subtype"], $this->recur["start"], $this->recur["end"], $this->recur["startocc"], $this->recur["endocc"])) {
573
+                return;
574
+            }
575
+
576
+            $rdata = pack("CCCCCCV", 0x04, 0x30, 0x04, 0x30, (int) $this->recur["type"], 0x20, (int) $this->recur["subtype"]);
577
+
578
+            $weekstart = 1; // monday
579
+            $forwardcount = 0;
580
+            $restocc = 0;
581
+            $dayofweek = (int) gmdate("w", (int) $this->recur["start"]); // 0 (for Sunday) through 6 (for Saturday)
582
+
583
+            $term = (int) $this->recur["type"];
584
+
585
+            switch ($term) {
586
+                case 0x0A:
587
+                    // Daily
588
+                    if (!isset($this->recur["everyn"])) {
589
+                        return;
590
+                    }
591
+
592
+                    if ($this->recur["subtype"] == 1) {
593
+                        // Daily every workday
594
+                        $rdata .= pack("VVVV", (6 * 24 * 60), 1, 0, 0x3E);
595
+                    }
596
+                    else {
597
+                        // Daily every N days (everyN in minutes)
598
+
599
+                        $everyn = ((int) $this->recur["everyn"]) / 1440;
600
+
601
+                        // Calc first occ
602
+                        $firstocc = $this->unixDataToRecurData($this->recur["start"]) % ((int) $this->recur["everyn"]);
603
+
604
+                        $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], $this->recur["regen"] ? 1 : 0);
605
+                    }
606
+
607
+                    break;
608
+
609
+                case 0x0B:
610
+                    // Weekly
611
+                    if (!isset($this->recur["everyn"])) {
612
+                        return;
613
+                    }
614
+
615
+                    if (!$this->recur["regen"] && !isset($this->recur["weekdays"])) {
616
+                        return;
617
+                    }
618
+
619
+                    // No need to calculate startdate if sliding flag was set.
620
+                    if (!$this->recur['regen']) {
621
+                        // Calculate start date of recurrence
622
+
623
+                        // Find the first day that matches one of the weekdays selected
624
+                        $daycount = 0;
625
+                        $dayskip = -1;
626
+                        for ($j = 0; $j < 7; ++$j) {
627
+                            if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
628
+                                if ($dayskip == -1) {
629
+                                    $dayskip = $j;
630
+                                }
631
+
632
+                                ++$daycount;
633
+                            }
634
+                        }
635
+
636
+                        // $dayskip is the number of days to skip from the startdate until the first occurrence
637
+                        // $daycount is the number of days per week that an occurrence occurs
638
+
639
+                        $weekskip = 0;
640
+                        if (($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek + $dayskip) > 6) {
641
+                            $weekskip = 1;
642
+                        }
643
+
644
+                        // Check if the recurrence ends after a number of occurrences, in that case we must calculate the
645
+                        // remaining occurrences based on the start of the recurrence.
646
+                        if (((int) $this->recur["term"]) == 0x22) {
647
+                            // $weekskip is the amount of weeks to skip from the startdate before the first occurrence
648
+                            // $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that
649
+                            // is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over
650
+                            // (eg when numoccur = 2, and daycount = 1)
651
+                            $forwardcount = floor((int) ($this->recur["numoccur"] - 1) / $daycount);
652
+
653
+                            // $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one
654
+                            // for the occurrence on the first day
655
+                            $restocc = ((int) $this->recur["numoccur"]) - ($forwardcount * $daycount) - 1;
656
+
657
+                            // $forwardcount is now the number of weeks we can go forward and still be inside the recurrence
658
+                            $forwardcount *= (int) $this->recur["everyn"];
659
+                        }
660
+
661
+                        // The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week)
662
+                        $this->recur["start"] = ((int) $this->recur["start"]) + ($dayskip * 24 * 60 * 60) + ($weekskip * (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60);
663
+                    }
664
+
665
+                    // Calc first occ
666
+                    $firstocc = ($this->unixDataToRecurData($this->recur["start"])) % (((int) $this->recur["everyn"]) * 7 * 24 * 60);
667
+
668
+                    $firstocc -= (((int) gmdate("w", (int) $this->recur["start"])) - 1) * 24 * 60;
669
+
670
+                    if ($this->recur["regen"]) {
671
+                        $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], 1);
672
+                    }
673
+                    else {
674
+                        $rdata .= pack("VVVV", $firstocc, (int) $this->recur["everyn"], 0, (int) $this->recur["weekdays"]);
675
+                    }
676
+
677
+                    break;
678
+
679
+                case 0x0C:
680
+                    // Monthly
681
+                case 0x0D:
682
+                    // Yearly
683
+                    if (!isset($this->recur["everyn"])) {
684
+                        return;
685
+                    }
686
+                    if ($term == 0x0D /* yearly */ && !isset($this->recur["month"])) {
687
+                        return;
688
+                    }
689
+
690
+                    if ($term == 0x0C /* monthly */) {
691
+                        $everyn = (int) $this->recur["everyn"];
692
+                    }
693
+                    else {
694
+                        $everyn = $this->recur["regen"] ? ((int) $this->recur["everyn"]) * 12 : 12;
695
+                    }
696
+
697
+                    // Get montday/month/year of original start
698
+                    $curmonthday = gmdate("j", (int) $this->recur["start"]);
699
+                    $curyear = gmdate("Y", (int) $this->recur["start"]);
700
+                    $curmonth = gmdate("n", (int) $this->recur["start"]);
701
+
702
+                    // Check if the recurrence ends after a number of occurrences, in that case we must calculate the
703
+                    // remaining occurrences based on the start of the recurrence.
704
+                    if (((int) $this->recur["term"]) == 0x22) {
705
+                        // $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus
706
+                        // one to make sure there are always at least one occurrence left)
707
+                        $forwardcount = ((((int) $this->recur["numoccur"]) - 1) * $everyn);
708
+                    }
709
+
710
+                    // Get month for yearly on D'th day of month M
711
+                    if ($term == 0x0D /* yearly */) {
712
+                        $selmonth = floor(((int) $this->recur["month"]) / (24 * 60 * 29)) + 1; // 1=jan, 2=feb, eg
713
+                    }
714
+
715
+                    switch ((int) $this->recur["subtype"]) {
716
+                        // on D day of every M month
717
+                        case 2:
718
+                            if (!isset($this->recur["monthday"])) {
719
+                                return;
720
+                            }
721
+                            // Recalc startdate
722
+
723
+                            // Set on the right begin day
724
+
725
+                            // Go the beginning of the month
726
+                            $this->recur["start"] -= ($curmonthday - 1) * 24 * 60 * 60;
727
+                            // Go the the correct month day
728
+                            $this->recur["start"] += (((int) $this->recur["monthday"]) - 1) * 24 * 60 * 60;
729
+
730
+                            // If the previous calculation gave us a start date different than the original start date, then we need to skip to the first occurrence
731
+                            if (($term == 0x0C /* monthly */ && ((int) $this->recur["monthday"]) < $curmonthday) ||
732
+                                ($term == 0x0D /* yearly */ && ($selmonth != $curmonth || ($selmonth == $curmonth && ((int) $this->recur["monthday"]) < $curmonthday)))) {
733
+                                if ($term == 0x0D /* yearly */) {
734
+                                    if ($curmonth > $selmonth) {// go to next occurrence in 'everyn' months minus difference in first occurrence and original date
735
+                                        $count = $everyn - ($curmonth - $selmonth);
736
+                                    }
737
+                                    elseif ($curmonth < $selmonth) {// go to next occurrence upto difference in first occurrence and original date
738
+                                        $count = $selmonth - $curmonth;
739
+                                    }
740
+                                    else {
741
+                                        // Go to next occurrence while recurrence start date is greater than occurrence date but within same month
742
+                                        if (((int) $this->recur["monthday"]) < $curmonthday) {
743
+                                            $count = $everyn;
744
+                                        }
745
+                                    }
746
+                                }
747
+                                else {
748
+                                    $count = $everyn; // Monthly, go to next occurrence in 'everyn' months
749
+                                }
750
+
751
+                                // Forward by $count months. This is done by getting the number of days in that month and forwarding that many days
752
+                                for ($i = 0; $i < $count; ++$i) {
753
+                                    $this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth);
754
+
755
+                                    if ($curmonth == 12) {
756
+                                        ++$curyear;
757
+                                        $curmonth = 0;
758
+                                    }
759
+                                    ++$curmonth;
760
+                                }
761
+                            }
762
+
763
+                            // "start" is now pointing to the first occurrence, except that it will overshoot if the
764
+                            // month in which it occurs has less days than specified as the day of the month. So 31st
765
+                            // of each month will overshoot in february (29 days). We compensate for that by checking
766
+                            // if the day of the month we got is wrong, and then back up to the last day of the previous
767
+                            // month.
768
+                            if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
769
+                                gmdate("j", ((int) $this->recur["start"])) < ((int) $this->recur["monthday"])) {
770
+                                $this->recur["start"] -= gmdate("j", ((int) $this->recur["start"])) * 24 * 60 * 60;
771
+                            }
772
+
773
+                            // "start" is now the first occurrence
774
+
775
+                            if ($term == 0x0C /* monthly */) {
776
+                                // Calc first occ
777
+                                $monthIndex = ((((12 % $everyn) * ((((int) gmdate("Y", $this->recur["start"])) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
778
+
779
+                                $firstocc = 0;
780
+                                for ($i = 0; $i < $monthIndex; ++$i) {
781
+                                    $firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
782
+                                }
783
+
784
+                                $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
785
+                            }
786
+                            else {
787
+                                // Calc first occ
788
+                                $firstocc = 0;
789
+                                $monthIndex = (int) gmdate("n", $this->recur["start"]);
790
+                                for ($i = 1; $i < $monthIndex; ++$i) {
791
+                                    $firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
792
+                                }
793
+
794
+                                $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
795
+                            }
796
+
797
+                            break;
798
+
799
+                        case 3:
800
+                            // monthly: on Nth weekday of every M month
801
+                            // yearly: on Nth weekday of M month
802
+                            if (!isset($this->recur["weekdays"], $this->recur["nday"])) {
803
+                                return;
804
+                            }
805
+
806
+                            $weekdays = (int) $this->recur["weekdays"];
807
+                            $nday = (int) $this->recur["nday"];
808
+
809
+                            // Calc startdate
810
+                            $monthbegindow = (int) $this->recur["start"];
811
+
812
+                            if ($nday == 5) {
813
+                                // Set date on the last day of the last month
814
+                                $monthbegindow += (gmdate("t", $monthbegindow) - gmdate("j", $monthbegindow)) * 24 * 60 * 60;
815
+                            }
816
+                            else {
817
+                                // Set on the first day of the month
818
+                                $monthbegindow -= ((gmdate("j", $monthbegindow) - 1) * 24 * 60 * 60);
819
+                            }
820
+
821
+                            if ($term == 0x0D /* yearly */) {
822
+                                // Set on right month
823
+                                if ($selmonth < $curmonth) {
824
+                                    $tmp = 12 - $curmonth + $selmonth;
825
+                                }
826
+                                else {
827
+                                    $tmp = ($selmonth - $curmonth);
828
+                                }
829
+
830
+                                for ($i = 0; $i < $tmp; ++$i) {
831
+                                    $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
832
+
833
+                                    if ($curmonth == 12) {
834
+                                        ++$curyear;
835
+                                        $curmonth = 0;
836
+                                    }
837
+                                    ++$curmonth;
838
+                                }
839
+                            }
840
+                            else {
841
+                                // Check or you exist in the right month
842
+
843
+                                $dayofweek = gmdate("w", $monthbegindow);
844
+                                for ($i = 0; $i < 7; ++$i) {
845
+                                    if ($nday == 5 && (($dayofweek - $i) % 7 >= 0) && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
846
+                                        $day = gmdate("j", $monthbegindow) - $i;
847
+
848
+                                        break;
849
+                                    }
850
+                                    if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
851
+                                        $day = (($nday - 1) * 7) + ($i + 1);
852
+
853
+                                        break;
854
+                                    }
855
+                                }
856
+
857
+                                // Goto the next X month
858
+                                if (isset($day) && ($day < gmdate("j", (int) $this->recur["start"]))) {
859
+                                    if ($nday == 5) {
860
+                                        $monthbegindow += 24 * 60 * 60;
861
+                                        if ($curmonth == 12) {
862
+                                            ++$curyear;
863
+                                            $curmonth = 0;
864
+                                        }
865
+                                        ++$curmonth;
866
+                                    }
867
+
868
+                                    for ($i = 0; $i < $everyn; ++$i) {
869
+                                        $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
870
+
871
+                                        if ($curmonth == 12) {
872
+                                            ++$curyear;
873
+                                            $curmonth = 0;
874
+                                        }
875
+                                        ++$curmonth;
876
+                                    }
877
+
878
+                                    if ($nday == 5) {
879
+                                        $monthbegindow -= 24 * 60 * 60;
880
+                                    }
881
+                                }
882
+                            }
883
+
884
+                            // FIXME: weekstart?
885
+
886
+                            $day = 0;
887
+                            // Set start on the right day
888
+                            $dayofweek = gmdate("w", $monthbegindow);
889
+                            for ($i = 0; $i < 7; ++$i) {
890
+                                if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
891
+                                    $day = $i;
892
+
893
+                                    break;
894
+                                }
895
+                                if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
896
+                                    $day = ($nday - 1) * 7 + ($i + 1);
897
+
898
+                                    break;
899
+                                }
900
+                            }
901
+                            if ($nday == 5) {
902
+                                $monthbegindow -= $day * 24 * 60 * 60;
903
+                            }
904
+                            else {
905
+                                $monthbegindow += ($day - 1) * 24 * 60 * 60;
906
+                            }
907
+
908
+                            $firstocc = 0;
909
+
910
+                            if ($term == 0x0C /* monthly */) {
911
+                                // Calc first occ
912
+                                $monthIndex = ((((12 % $everyn) * (((int) gmdate("Y", $this->recur["start"]) - 1601) % $everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1)) % $everyn;
913
+
914
+                                for ($i = 0; $i < $monthIndex; ++$i) {
915
+                                    $firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), ($i % 12) + 1) / 60;
916
+                                }
917
+
918
+                                $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
919
+                            }
920
+                            else {
921
+                                // Calc first occ
922
+                                $monthIndex = (int) gmdate("n", $this->recur["start"]);
923
+
924
+                                for ($i = 1; $i < $monthIndex; ++$i) {
925
+                                    $firstocc += $this->getMonthInSeconds(1601 + floor($i / 12), $i) / 60;
926
+                                }
927
+
928
+                                $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
929
+                            }
930
+
931
+                            break;
932
+                    }
933
+
934
+                    break;
935
+            }
936
+
937
+            if (!isset($this->recur["term"])) {
938
+                return;
939
+            }
940
+
941
+            // Terminate
942
+            $term = (int) $this->recur["term"];
943
+            $rdata .= pack("CCCC", $term, 0x20, 0x00, 0x00);
944
+
945
+            switch ($term) {
946
+                // After the given enddate
947
+                case 0x21:
948
+                    $rdata .= pack("V", 10);
949
+
950
+                    break;
951
+                // After a number of times
952
+                case 0x22:
953
+                    if (!isset($this->recur["numoccur"])) {
954
+                        return;
955
+                    }
956
+
957
+                    $rdata .= pack("V", (int) $this->recur["numoccur"]);
958
+
959
+                    break;
960
+                // Never ends
961
+                case 0x23:
962
+                    $rdata .= pack("V", 0);
963
+
964
+                    break;
965
+            }
966
+
967
+            // Strange little thing for the recurrence type "every workday"
968
+            if (((int) $this->recur["type"]) == 0x0B && ((int) $this->recur["subtype"]) == 1) {
969
+                $rdata .= pack("V", 1);
970
+            }
971
+            else { // Other recurrences
972
+                $rdata .= pack("V", 0);
973
+            }
974
+
975
+            // Exception data
976
+
977
+            // Get all exceptions
978
+            $deleted_items = $this->recur["deleted_occurrences"];
979
+            $changed_items = $this->recur["changed_occurrences"];
980
+
981
+            // Merge deleted and changed items into one list
982
+            $items = $deleted_items;
983
+
984
+            foreach ($changed_items as $changed_item) {
985
+                array_push($items, $changed_item["basedate"]);
986
+            }
987
+
988
+            sort($items);
989
+
990
+            // Add the merged list in to the rdata
991
+            $rdata .= pack("V", count($items));
992
+            foreach ($items as $item) {
993
+                $rdata .= pack("V", $this->unixDataToRecurData($item));
994
+            }
995
+
996
+            // Loop through the changed exceptions (not deleted)
997
+            $rdata .= pack("V", count($changed_items));
998
+            $items = [];
999
+
1000
+            foreach ($changed_items as $changed_item) {
1001
+                $items[] = $this->dayStartOf($changed_item["start"]);
1002
+            }
1003
+
1004
+            sort($items);
1005
+
1006
+            // Add the changed items list int the rdata
1007
+            foreach ($items as $item) {
1008
+                $rdata .= pack("V", $this->unixDataToRecurData($item));
1009
+            }
1010
+
1011
+            // Set start date
1012
+            $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["start"]));
1013
+
1014
+            // Set enddate
1015
+            switch ($term) {
1016
+                // After the given enddate
1017
+                case 0x21:
1018
+                    $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1019
+
1020
+                    break;
1021
+                // After a number of times
1022
+                case 0x22:
1023
+                    // @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour
1024
+                    $occenddate = (int) $this->recur["start"];
1025
+
1026
+                    switch ((int) $this->recur["type"]) {
1027
+                        case 0x0A: // daily
1028
+                            if ($this->recur["subtype"] == 1) {
1029
+                                // Daily every workday
1030
+                                $restocc = (int) $this->recur["numoccur"];
1031
+
1032
+                                // Get starting weekday
1033
+                                $nowtime = $this->gmtime($occenddate);
1034
+                                $j = $nowtime["tm_wday"];
1035
+
1036
+                                while (1) {
1037
+                                    if (($j % 7) > 0 && ($j % 7) < 6) {
1038
+                                        --$restocc;
1039
+                                    }
1040
+
1041
+                                    ++$j;
1042
+
1043
+                                    if ($restocc <= 0) {
1044
+                                        break;
1045
+                                    }
1046
+
1047
+                                    $occenddate += 24 * 60 * 60;
1048
+                                }
1049
+                            }
1050
+                            else {
1051
+                                // -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence)
1052
+                                $occenddate += (((int) $this->recur["everyn"]) * 60 * (((int) $this->recur["numoccur"] - 1)));
1053
+                            }
1054
+
1055
+                            break;
1056
+
1057
+                        case 0x0B: // weekly
1058
+                            // Needed values
1059
+                            // $forwardcount - number of weeks we can skip forward
1060
+                            // $restocc - number of remaining occurrences after the week skip
1061
+
1062
+                            // Add the weeks till the last item
1063
+                            $occenddate += ($forwardcount * 7 * 24 * 60 * 60);
1064
+
1065
+                            $dayofweek = gmdate("w", $occenddate);
1066
+
1067
+                            // Loop through the last occurrences until we have had them all
1068
+                            for ($j = 1; $restocc > 0; ++$j) {
1069
+                                // Jump to the next week (which may be N weeks away) when going over the week boundary
1070
+                                if ((($dayofweek + $j) % 7) == $weekstart) {
1071
+                                    $occenddate += (((int) $this->recur["everyn"]) - 1) * 7 * 24 * 60 * 60;
1072
+                                }
1073
+
1074
+                                // If this is a matching day, once less occurrence to process
1075
+                                if (((int) $this->recur["weekdays"]) & (1 << (($dayofweek + $j) % 7))) {
1076
+                                    --$restocc;
1077
+                                }
1078
+
1079
+                                // Next day
1080
+                                $occenddate += 24 * 60 * 60;
1081
+                            }
1082
+
1083
+                            break;
1084
+
1085
+                        case 0x0C: // monthly
1086
+                        case 0x0D: // yearly
1087
+                            $curyear = gmdate("Y", (int) $this->recur["start"]);
1088
+                            $curmonth = gmdate("n", (int) $this->recur["start"]);
1089
+                            // $forwardcount = months
1090
+
1091
+                            switch ((int) $this->recur["subtype"]) {
1092
+                                case 2: // on D day of every M month
1093
+                                    while ($forwardcount > 0) {
1094
+                                        $occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1095
+
1096
+                                        if ($curmonth >= 12) {
1097
+                                            $curmonth = 1;
1098
+                                            ++$curyear;
1099
+                                        }
1100
+                                        else {
1101
+                                            ++$curmonth;
1102
+                                        }
1103
+                                        --$forwardcount;
1104
+                                    }
1105
+
1106
+                                    // compensation between 28 and 31
1107
+                                    if (((int) $this->recur["monthday"]) >= 28 && ((int) $this->recur["monthday"]) <= 31 &&
1108
+                                        gmdate("j", $occenddate) < ((int) $this->recur["monthday"])) {
1109
+                                        if (gmdate("j", $occenddate) < 28) {
1110
+                                            $occenddate -= gmdate("j", $occenddate) * 24 * 60 * 60;
1111
+                                        }
1112
+                                        else {
1113
+                                            $occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1114
+                                        }
1115
+                                    }
1116
+
1117
+                                    break;
1118
+
1119
+                                case 3: // on Nth weekday of every M month
1120
+                                    $nday = (int) $this->recur["nday"]; // 1 tot 5
1121
+                                    $weekdays = (int) $this->recur["weekdays"];
1122
+
1123
+                                    while ($forwardcount > 0) {
1124
+                                        $occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1125
+                                        if ($curmonth >= 12) {
1126
+                                            $curmonth = 1;
1127
+                                            ++$curyear;
1128
+                                        }
1129
+                                        else {
1130
+                                            ++$curmonth;
1131
+                                        }
1132
+
1133
+                                        --$forwardcount;
1134
+                                    }
1135
+
1136
+                                    if ($nday == 5) {
1137
+                                        // Set date on the last day of the last month
1138
+                                        $occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 * 60;
1139
+                                    }
1140
+                                    else {
1141
+                                        // Set date on the first day of the last month
1142
+                                        $occenddate -= (gmdate("j", $occenddate) - 1) * 24 * 60 * 60;
1143
+                                    }
1144
+
1145
+                                    $dayofweek = gmdate("w", $occenddate);
1146
+                                    for ($i = 0; $i < 7; ++$i) {
1147
+                                        if ($nday == 5 && (($dayofweek - $i) % 7) >= 0 && (1 << (($dayofweek - $i) % 7)) & $weekdays) {
1148
+                                            $occenddate -= $i * 24 * 60 * 60;
1149
+
1150
+                                            break;
1151
+                                        }
1152
+                                        if ($nday != 5 && (1 << (($dayofweek + $i) % 7)) & $weekdays) {
1153
+                                            $occenddate += ($i + (($nday - 1) * 7)) * 24 * 60 * 60;
1154
+
1155
+                                            break;
1156
+                                        }
1157
+                                    }
1158
+
1159
+                                break; // case 3:
1160
+                                }
1161
+
1162
+                            break;
1163
+                    }
1164
+
1165
+                    if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX) {
1166
+                        $occenddate = PHP_INT_MAX;
1167
+                    }
1168
+
1169
+                    $this->recur["end"] = $occenddate;
1170
+
1171
+                    $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1172
+
1173
+                    break;
1174
+                // Never ends
1175
+                case 0x23:
1176
+                default:
1177
+                    $this->recur["end"] = 0x7FFFFFFF; // max date -> 2038
1178
+                    $rdata .= pack("V", 0x5AE980DF);
1179
+
1180
+                    break;
1181
+            }
1182
+
1183
+            // UTC date
1184
+            $utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]);
1185
+            $utcend = $this->toGMT($this->tz, (int) $this->recur["end"]);
1186
+
1187
+            // utc date+time
1188
+            $utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"]) * 60) : $utcstart;
1189
+            $utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart;
1190
+
1191
+            $propsToSet = [];
1192
+            // update reminder time
1193
+            $propsToSet[$this->proptags["reminder_time"]] = $utcfirstoccstartdatetime;
1194
+
1195
+            // update first occurrence date
1196
+            $propsToSet[$this->proptags["startdate"]] = $propsToSet[$this->proptags["commonstart"]] = $utcfirstoccstartdatetime;
1197
+            $propsToSet[$this->proptags["duedate"]] = $propsToSet[$this->proptags["commonend"]] = $utcfirstoccenddatetime;
1198
+
1199
+            // Set Outlook properties, if it is an appointment
1200
+            if (isset($this->messageprops[$this->proptags["message_class"]]) && $this->messageprops[$this->proptags["message_class"]] == "IPM.Appointment") {
1201
+                // update real begin and real end date
1202
+                $propsToSet[$this->proptags["startdate_recurring"]] = $utcstart;
1203
+                $propsToSet[$this->proptags["enddate_recurring"]] = $utcend;
1204
+
1205
+                // recurrencetype
1206
+                // Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype
1207
+                $propsToSet[$this->proptags["recurrencetype"]] = ((int) $this->recur["type"]) - 0x9;
1208
+
1209
+                // set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting
1210
+                $propsToSet[$this->proptags["side_effects"]] = 369;
1211
+            }
1212
+            else {
1213
+                $propsToSet[$this->proptags["side_effects"]] = 3441;
1214
+            }
1215
+
1216
+            // FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog
1217
+            // Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset
1218
+            // to the 'next' occurrence; this makes sure that deleting the next occurrence will correctly set the reminder to
1219
+            // the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time)
1220
+            // with the reminder flag set.
1221
+            $reminderprops = mapi_getprops($this->message, [$this->proptags["reminder_minutes"]]);
1222
+            if (isset($reminderprops[$this->proptags["reminder_minutes"]])) {
1223
+                $occ = false;
1224
+                $occurrences = $this->getItems(time(), 0x7FF00000, 3, true);
1225
+
1226
+                for ($i = 0, $len = count($occurrences); $i < $len; ++$i) {
1227
+                    // This will actually also give us appointments that have already started, but not yet ended. Since we want the next
1228
+                    // reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum
1229
+                    // number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case:
1230
+                    // time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item
1231
+                    // can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped.
1232
+
1233
+                    if (($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) {
1234
+                        $occ = $occurrences[$i];
1235
+
1236
+                        break;
1237
+                    }
1238
+                }
1239
+
1240
+                if ($occ) {
1241
+                    $propsToSet[$this->proptags["flagdueby"]] = $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60);
1242
+                }
1243
+                else {
1244
+                    // Last reminder passed, no reminders any more.
1245
+                    $propsToSet[$this->proptags["reminder"]] = false;
1246
+                    $propsToSet[$this->proptags["flagdueby"]] = 0x7FF00000;
1247
+                }
1248
+            }
1249
+
1250
+            // Default data
1251
+            // Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information)
1252
+            $rdata .= pack("VCCCC", 0x00003006, 0x08, 0x30, 0x00, 0x00);
1253
+
1254
+            if (isset($this->recur["startocc"], $this->recur["endocc"])) {
1255
+                // Set start and endtime in minutes
1256
+                $rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]);
1257
+            }
1258
+
1259
+            // Detailed exception data
1260
+
1261
+            $changed_items = $this->recur["changed_occurrences"];
1262
+
1263
+            $rdata .= pack("v", count($changed_items));
1264
+
1265
+            foreach ($changed_items as $changed_item) {
1266
+                // Set start and end time of exception
1267
+                $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1268
+                $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1269
+                $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1270
+
1271
+                // Bitmask
1272
+                $bitmask = 0;
1273
+
1274
+                // Check for changed strings
1275
+                if (isset($changed_item["subject"])) {
1276
+                    $bitmask |= 1 << 0;
1277
+                }
1278
+
1279
+                if (isset($changed_item["remind_before"])) {
1280
+                    $bitmask |= 1 << 2;
1281
+                }
1282
+
1283
+                if (isset($changed_item["reminder_set"])) {
1284
+                    $bitmask |= 1 << 3;
1285
+                }
1286
+
1287
+                if (isset($changed_item["location"])) {
1288
+                    $bitmask |= 1 << 4;
1289
+                }
1290
+
1291
+                if (isset($changed_item["busystatus"])) {
1292
+                    $bitmask |= 1 << 5;
1293
+                }
1294
+
1295
+                if (isset($changed_item["alldayevent"])) {
1296
+                    $bitmask |= 1 << 7;
1297
+                }
1298
+
1299
+                if (isset($changed_item["label"])) {
1300
+                    $bitmask |= 1 << 8;
1301
+                }
1302
+
1303
+                $rdata .= pack("v", $bitmask);
1304
+
1305
+                // Set "subject"
1306
+                if (isset($changed_item["subject"])) {
1307
+                    // convert utf-8 to non-unicode blob string (us-ascii?)
1308
+                    $subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]);
1309
+                    $length = strlen($subject);
1310
+                    $rdata .= pack("vv", $length + 1, $length);
1311
+                    $rdata .= pack("a" . $length, $subject);
1312
+                }
1313
+
1314
+                if (isset($changed_item["remind_before"])) {
1315
+                    $rdata .= pack("V", $changed_item["remind_before"]);
1316
+                }
1317
+
1318
+                if (isset($changed_item["reminder_set"])) {
1319
+                    $rdata .= pack("V", $changed_item["reminder_set"]);
1320
+                }
1321
+
1322
+                if (isset($changed_item["location"])) {
1323
+                    $location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]);
1324
+                    $length = strlen($location);
1325
+                    $rdata .= pack("vv", $length + 1, $length);
1326
+                    $rdata .= pack("a" . $length, $location);
1327
+                }
1328
+
1329
+                if (isset($changed_item["busystatus"])) {
1330
+                    $rdata .= pack("V", $changed_item["busystatus"]);
1331
+                }
1332
+
1333
+                if (isset($changed_item["alldayevent"])) {
1334
+                    $rdata .= pack("V", $changed_item["alldayevent"]);
1335
+                }
1336
+
1337
+                if (isset($changed_item["label"])) {
1338
+                    $rdata .= pack("V", $changed_item["label"]);
1339
+                }
1340
+            }
1341
+
1342
+            $rdata .= pack("V", 0);
1343
+
1344
+            // write extended data
1345
+            foreach ($changed_items as $changed_item) {
1346
+                $rdata .= pack("V", 0);
1347
+                if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1348
+                    $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1349
+                    $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1350
+                    $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1351
+                }
1352
+
1353
+                if (isset($changed_item["subject"])) {
1354
+                    $subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]);
1355
+                    $length = iconv_strlen($subject, "UCS-2LE");
1356
+                    $rdata .= pack("v", $length);
1357
+                    $rdata .= pack("a" . $length * 2, $subject);
1358
+                }
1359
+
1360
+                if (isset($changed_item["location"])) {
1361
+                    $location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]);
1362
+                    $length = iconv_strlen($location, "UCS-2LE");
1363
+                    $rdata .= pack("v", $length);
1364
+                    $rdata .= pack("a" . $length * 2, $location);
1365
+                }
1366
+
1367
+                if (isset($changed_item["subject"]) || isset($changed_item["location"])) {
1368
+                    $rdata .= pack("V", 0);
1369
+                }
1370
+            }
1371
+
1372
+            $rdata .= pack("V", 0);
1373
+
1374
+            // Set props
1375
+            $propsToSet[$this->proptags["recurring_data"]] = $rdata;
1376
+            $propsToSet[$this->proptags["recurring"]] = true;
1377
+
1378
+            if (isset($this->tz) && $this->tz) {
1379
+                $timezone = "GMT";
1380
+                if ($this->tz["timezone"] != 0) {
1381
+                    // Create user readable timezone information
1382
+                    $timezone = sprintf(
1383
+                        "(GMT %s%02d:%02d)",
1384
+                        (-$this->tz["timezone"] > 0 ? "+" : "-"),
1385
+                        abs($this->tz["timezone"] / 60),
1386
+                        abs($this->tz["timezone"] % 60)
1387
+                    );
1388
+                }
1389
+                $propsToSet[$this->proptags["timezone_data"]] = $this->getTimezoneData($this->tz);
1390
+                $propsToSet[$this->proptags["timezone"]] = $timezone;
1391
+            }
1392
+            mapi_setprops($this->message, $propsToSet);
1393
+        }
1394
+
1395
+        /**
1396
+         * Function which converts a recurrence date timestamp to an unix date timestamp.
1397
+         *
1398
+         * @author Steve Hardy
1399
+         *
1400
+         * @param int $rdate the date which will be converted
1401
+         *
1402
+         * @return int the converted date
1403
+         */
1404
+        public function recurDataToUnixData($rdate) {
1405
+            return ($rdate - 194074560) * 60;
1406
+        }
1407
+
1408
+        /**
1409
+         * Function which converts an unix date timestamp to recurrence date timestamp.
1410
+         *
1411
+         * @author Johnny Biemans
1412
+         *
1413
+         * @param Date $date the date which will be converted
1414
+         *
1415
+         * @return int the converted date in minutes
1416
+         */
1417
+        public function unixDataToRecurData($date) {
1418
+            return ($date / 60) + 194074560;
1419
+        }
1420
+
1421
+        /**
1422
+         * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1423
+         *
1424
+         * @author Steve Hardy
1425
+         *
1426
+         * @param mixed $ts
1427
+         */
1428
+        public function GetTZOffset($ts) {
1429
+            $Offset = date("O", $ts);
1430
+
1431
+            $Parity = $Offset < 0 ? -1 : 1;
1432
+            $Offset = $Parity * $Offset;
1433
+            $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
1434
+
1435
+            return $Parity * $Offset;
1436
+        }
1437
+
1438
+        /**
1439
+         * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves.
1440
+         *
1441
+         * @author Steve Hardy
1442
+         *
1443
+         * @param Date $time
1444
+         *
1445
+         * @return Date GMT Time
1446
+         */
1447
+        public function gmtime($time) {
1448
+            $TZOffset = $this->GetTZOffset($time);
1449
+
1450
+            $t_time = $time - $TZOffset * 60; # Counter adjust for localtime()
1451
+
1452
+            return localtime($t_time, 1);
1453
+        }
1454
+
1455
+        public function isLeapYear($year) {
1456
+            return $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0);
1457
+        }
1458
+
1459
+        public function getMonthInSeconds($year, $month) {
1460
+            if (in_array($month, [1, 3, 5, 7, 8, 10, 12])) {
1461
+                $day = 31;
1462
+            }
1463
+            elseif (in_array($month, [4, 6, 9, 11])) {
1464
+                $day = 30;
1465
+            }
1466
+            else {
1467
+                $day = 28;
1468
+                if ($this->isLeapYear($year) == 1) {
1469
+                    ++$day;
1470
+                }
1471
+            }
1472
+
1473
+            return $day * 24 * 60 * 60;
1474
+        }
1475
+
1476
+        /**
1477
+         * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour.
1478
+         *
1479
+         * @param int $year
1480
+         * @param int $month
1481
+         * @param int $week
1482
+         * @param int $day
1483
+         * @param int $hour
1484
+         *
1485
+         * @return returns the timestamp of the given date, timezone-independant
1486
+         */
1487
+        public function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour) {
1488
+            // get first day of month
1489
+            $date = gmmktime(0, 0, 0, $month, 0, $year + 1900);
1490
+
1491
+            // get wday info
1492
+            $gmdate = $this->gmtime($date);
1493
+
1494
+            $date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week
1495
+
1496
+            $date += $week * 7 * 24 * 60 * 60; // go to correct week nr
1497
+            $date += $day * 24 * 60 * 60;
1498
+            $date += $hour * 60 * 60;
1499
+
1500
+            $gmdate = $this->gmtime($date);
1501
+
1502
+            // if we are in the next month, then back up a week, because week '5' means
1503
+            // 'last week of month'
1504
+
1505
+            if ($month != $gmdate["tm_mon"] + 1) {
1506
+                $date -= 7 * 24 * 60 * 60;
1507
+            }
1508
+
1509
+            return $date;
1510
+        }
1511
+
1512
+        /**
1513
+         * getTimezone gives the timezone offset (in minutes) of the given
1514
+         * local date/time according to the given TZ info.
1515
+         *
1516
+         * @param mixed $tz
1517
+         * @param mixed $date
1518
+         */
1519
+        public function getTimezone($tz, $date) {
1520
+            // No timezone -> GMT (+0)
1521
+            if (!isset($tz["timezone"])) {
1522
+                return 0;
1523
+            }
1524
+
1525
+            $dst = false;
1526
+            $gmdate = $this->gmtime($date);
1527
+
1528
+            $dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]);
1529
+            $dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]);
1530
+
1531
+            if ($dststart <= $dstend) {
1532
+                // Northern hemisphere, eg DST is during Mar-Oct
1533
+                if ($date > $dststart && $date < $dstend) {
1534
+                    $dst = true;
1535
+                }
1536
+            }
1537
+            else {
1538
+                // Southern hemisphere, eg DST is during Oct-Mar
1539
+                if ($date < $dstend || $date > $dststart) {
1540
+                    $dst = true;
1541
+                }
1542
+            }
1543
+
1544
+            if ($dst) {
1545
+                return $tz["timezone"] + $tz["timezonedst"];
1546
+            }
1547
+
1548
+            return $tz["timezone"];
1549
+        }
1550
+
1551
+        /**
1552
+         * getWeekNr() returns the week nr of the month (ie first week of february is 1).
1553
+         *
1554
+         * @param mixed $date
1555
+         */
1556
+        public function getWeekNr($date) {
1557
+            $gmdate = gmtime($date);
1558
+            $gmdate["tm_mday"] = 0;
1559
+
1560
+            return strftime("%W", $date) - strftime("%W", gmmktime($gmdate)) + 1;
1561
+        }
1562
+
1563
+        /**
1564
+         * parseTimezone parses the timezone as specified in named property 0x8233
1565
+         * in Outlook calendar messages. Returns the timezone in minutes negative
1566
+         * offset (GMT +2:00 -> -120).
1567
+         *
1568
+         * @param mixed $data
1569
+         */
1570
+        public function parseTimezone($data) {
1571
+            if (strlen($data) < 48) {
1572
+                return;
1573
+            }
1574
+
1575
+            return unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data);
1576
+        }
1577
+
1578
+        public function getTimezoneData($tz) {
1579
+            return pack("lllllvvllvlvvlv", $tz["timezone"], 0, $tz["timezonedst"], 0, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendhour"], 0, 0, 0, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststarthour"], 0, 0);
1580
+        }
1581
+
1582
+        /**
1583
+         * createTimezone creates the timezone as specified in the named property 0x8233
1584
+         * see also parseTimezone()
1585
+         * $tz is an array with the timezone data.
1586
+         *
1587
+         * @param mixed $tz
1588
+         */
1589
+        public function createTimezone($tz) {
1590
+            return pack(
1591
+                "lxxxxlxxxxlvvxxxxxxxxxxlvvxxxxxx",
1592
+                $tz["timezone"],
1593
+                array_key_exists("timezonedst", $tz) ? $tz["timezonedst"] : 0,
1594
+                array_key_exists("dstendmonth", $tz) ? $tz["dstendmonth"] : 0,
1595
+                array_key_exists("dstendweek", $tz) ? $tz["dstendweek"] : 0,
1596
+                array_key_exists("dstendhour", $tz) ? $tz["dstendhour"] : 0,
1597
+                array_key_exists("dststartmonth", $tz) ? $tz["dststartmonth"] : 0,
1598
+                array_key_exists("dststartweek", $tz) ? $tz["dststartweek"] : 0,
1599
+                array_key_exists("dststarthour", $tz) ? $tz["dststarthour"] : 0
1600
+            );
1601
+        }
1602
+
1603
+        /**
1604
+         * toGMT returns a timestamp in GMT time for the time and timezone given.
1605
+         *
1606
+         * @param mixed $tz
1607
+         * @param mixed $date
1608
+         */
1609
+        public function toGMT($tz, $date) {
1610
+            if (!isset($tz['timezone'])) {
1611
+                return $date;
1612
+            }
1613
+            $offset = $this->getTimezone($tz, $date);
1614
+
1615
+            return $date + $offset * 60;
1616
+        }
1617
+
1618
+        /**
1619
+         * fromGMT returns a timestamp in the local timezone given from the GMT time given.
1620
+         *
1621
+         * @param mixed $tz
1622
+         * @param mixed $date
1623
+         */
1624
+        public function fromGMT($tz, $date) {
1625
+            $offset = $this->getTimezone($tz, $date);
1626
+
1627
+            return $date - $offset * 60;
1628
+        }
1629
+
1630
+        /**
1631
+         * Function to get timestamp of the beginning of the day of the timestamp given.
1632
+         *
1633
+         * @param date $date
1634
+         *
1635
+         * @return date timestamp referring to same day but at 00:00:00
1636
+         */
1637
+        public function dayStartOf($date) {
1638
+            $time1 = $this->gmtime($date);
1639
+
1640
+            return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900);
1641
+        }
1642
+
1643
+        /**
1644
+         * Function to get timestamp of the beginning of the month of the timestamp given.
1645
+         *
1646
+         * @param date $date
1647
+         *
1648
+         * @return date Timestamp referring to same month but on the first day, and at 00:00:00
1649
+         */
1650
+        public function monthStartOf($date) {
1651
+            $time1 = $this->gmtime($date);
1652
+
1653
+            return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900);
1654
+        }
1655
+
1656
+        /**
1657
+         * Function to get timestamp of the beginning of the year of the timestamp given.
1658
+         *
1659
+         * @param date $date
1660
+         *
1661
+         * @return date Timestamp referring to the same year but on Jan 01, at 00:00:00
1662
+         */
1663
+        public function yearStartOf($date) {
1664
+            $time1 = $this->gmtime($date);
1665
+
1666
+            return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900);
1667
+        }
1668
+
1669
+        /**
1670
+         * Function which returns the items in a given interval. This included expansion of the recurrence and
1671
+         * processing of exceptions (modified and deleted).
1672
+         *
1673
+         * @param string $entryid       the entryid of the message
1674
+         * @param array  $props         the properties of the message
1675
+         * @param date   $start         start time of the interval (GMT)
1676
+         * @param date   $end           end time of the interval (GMT)
1677
+         * @param mixed  $limit
1678
+         * @param mixed  $remindersonly
1679
+         */
1680
+        public function getItems($start, $end, $limit = 0, $remindersonly = false) {
1681
+            $items = [];
1682
+
1683
+            if (isset($this->recur)) {
1684
+                // Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which
1685
+                // exceptions are in range and have a reminder set
1686
+                if ($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) {
1687
+                    // Sort exceptions by start time
1688
+                    uasort($this->recur["changed_occurrences"], [$this, "sortExceptionStart"]);
1689
+
1690
+                    // Loop through all changed exceptions
1691
+                    foreach ($this->recur["changed_occurrences"] as $exception) {
1692
+                        // Check reminder set
1693
+                        if (!isset($exception["reminder"]) || $exception["reminder"] == false) {
1694
+                            continue;
1695
+                        }
1696
+
1697
+                        // Convert to GMT
1698
+                        $occstart = $this->toGMT($this->tz, $exception["start"]); // seb changed $tz to $this->tz
1699
+                        $occend = $this->toGMT($this->tz, $exception["end"]); // seb changed $tz to $this->tz
1700
+
1701
+                        // Check range criterium
1702
+                        if ($occstart > $end || $occend < $start) {
1703
+                            continue;
1704
+                        }
1705
+
1706
+                        // OK, add to items.
1707
+                        array_push($items, $this->getExceptionProperties($exception));
1708
+                        if ($limit && (count($items) == $limit)) {
1709
+                            break;
1710
+                        }
1711
+                    }
1712
+
1713
+                    uasort($items, [$this, "sortStarttime"]);
1714
+
1715
+                    return $items;
1716
+                }
1717
+
1718
+                // From here on, the dates of the occurrences are calculated in local time, so the days we're looking
1719
+                // at are calculated from the local time dates of $start and $end
1720
+                // TODO use one isset
1721
+                if (isset($this->recur['regen'], $this->action['datecompleted']) && $this->recur['regen']) {
1722
+                    $daystart = $this->dayStartOf($this->action['datecompleted']);
1723
+                }
1724
+                else {
1725
+                    $daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence
1726
+                }
1727
+
1728
+                // Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view
1729
+                // or the end of the recurrence, whichever comes first
1730
+                if ($end > $this->toGMT($this->tz, $this->recur["end"])) {
1731
+                    $rangeend = $this->toGMT($this->tz, $this->recur["end"]);
1732
+                }
1733
+                else {
1734
+                    $rangeend = $end;
1735
+                }
1736
+
1737
+                $dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend));
1738
+
1739
+                // Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range.
1740
+
1741
+                switch ($this->recur["type"]) {
1742
+                case 10:
1743
+                    // Daily
1744
+                    if ($this->recur["everyn"] <= 0) {
1745
+                        $this->recur["everyn"] = 1440;
1746
+                    }
1747
+
1748
+                    if ($this->recur["subtype"] == 0) {
1749
+                        // Every Nth day
1750
+                        for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) {
1751
+                            $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1752
+                        }
1753
+                    }
1754
+                    else {
1755
+                        // Every workday
1756
+                        for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440) {
1757
+                            $nowtime = $this->gmtime($now);
1758
+                            if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace
1759
+                                $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1760
+                            }
1761
+                        }
1762
+                    }
1763
+
1764
+                    break;
1765
+
1766
+                case 11:
1767
+                    // Weekly
1768
+                    if ($this->recur["everyn"] <= 0) {
1769
+                        $this->recur["everyn"] = 1;
1770
+                    }
1771
+
1772
+                    // If sliding flag is set then move to 'n' weeks
1773
+                    if ($this->recur['regen']) {
1774
+                        $daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]);
1775
+                    }
1776
+
1777
+                    for ($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"])) {
1778
+                        if ($this->recur['regen']) {
1779
+                            $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1780
+                        }
1781
+                        else {
1782
+                            // Loop through the whole following week to the first occurrence of the week, add each day that is specified
1783
+                            for ($wday = 0; $wday < 7; ++$wday) {
1784
+                                $daynow = $now + $wday * 60 * 60 * 24;
1785
+                                // checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item
1786
+                                if ($daynow <= $dayend) {
1787
+                                    $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1788
+                                    if (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"]))) { // Selected ?
1789
+                                        $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1790
+                                    }
1791
+                                }
1792
+                            }
1793
+                        }
1794
+                    }
1795
+
1796
+                    break;
1797
+
1798
+                case 12:
1799
+                    // Monthly
1800
+                    if ($this->recur["everyn"] <= 0) {
1801
+                        $this->recur["everyn"] = 1;
1802
+                    }
1803
+
1804
+                    // Loop through all months from start to end of occurrence, starting at beginning of first month
1805
+                    for ($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
1806
+                        if (isset($this->recur["monthday"]) && ($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months
1807
+                            $difference = 1;
1808
+                            if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) {
1809
+                                $difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1;
1810
+                            }
1811
+                            $daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60);
1812
+                            // checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item
1813
+                            if ($daynow <= $dayend) {
1814
+                                $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1815
+                            }
1816
+                        }
1817
+                        elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] of every N months
1818
+                            // Sanitize input
1819
+                            if ($this->recur["weekdays"] == 0) {
1820
+                                $this->recur["weekdays"] = 1;
1821
+                            }
1822
+
1823
+                            // If nday is not set to the last day in the month
1824
+                            if ($this->recur["nday"] < 5) {
1825
+                                // keep the track of no. of time correct selection pattern(like 2nd weekday, 4th fiday, etc.)is matched
1826
+                                $ndaycounter = 0;
1827
+                                // Find matching weekday in this month
1828
+                                for ($day = 0, $total = $this->daysInMonth($now, 1); $day < $total; ++$day) {
1829
+                                    $daynow = $now + $day * 60 * 60 * 24;
1830
+                                    $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1831
+
1832
+                                    if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1833
+                                        ++$ndaycounter;
1834
+                                    }
1835
+                                    // check the selected pattern is same as asked Nth weekday,If so set the firstday
1836
+                                    if ($this->recur["nday"] == $ndaycounter) {
1837
+                                        $firstday = $day;
1838
+
1839
+                                        break;
1840
+                                    }
1841
+                                }
1842
+                                // $firstday is the day of the month on which the asked pattern of nth weekday matches
1843
+                                $daynow = $now + $firstday * 60 * 60 * 24;
1844
+                            }
1845
+                            else {
1846
+                                // Find last day in the month ($now is the firstday of the month)
1847
+                                $NumDaysInMonth = $this->daysInMonth($now, 1);
1848
+                                $daynow = $now + (($NumDaysInMonth - 1) * 24 * 60 * 60);
1849
+
1850
+                                $nowtime = $this->gmtime($daynow);
1851
+                                while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) == 0) {
1852
+                                    $daynow -= 86400;
1853
+                                    $nowtime = $this->gmtime($daynow);
1854
+                                }
1855
+                            }
1856
+
1857
+                            /*
1858 1858
 							 * checks weather the next coming day in recurrence pattern is less than or equal to end day of the            * recurring item.Also check weather the coming day in recurrence pattern is greater than or equal to start * of recurring pattern, so that appointment that fall under the recurrence range are only displayed.
1859 1859
 							 */
1860
-							if ($daynow <= $dayend && $daynow >= $daystart) {
1861
-								$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1862
-							}
1863
-						}
1864
-						elseif ($this->recur['regen']) {
1865
-							$next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60);
1866
-							$now = $daystart + ($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60);
1867
-
1868
-							if ($now <= $dayend) {
1869
-								$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1870
-							}
1871
-						}
1872
-					}
1873
-
1874
-					break;
1875
-
1876
-				case 13:
1877
-					// Yearly
1878
-					if ($this->recur["everyn"] <= 0) {
1879
-						$this->recur["everyn"] = 12;
1880
-					}
1881
-
1882
-					for ($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
1883
-						if (isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month
1884
-							// recur["month"] is in minutes since the beginning of the year
1885
-							$month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11]
1886
-							$monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31]
1887
-							$monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month
1888
-							if ($monthday > $this->daysInMonth($monthstart, 1)) {
1889
-								$monthday = $this->daysInMonth($monthstart, 1);
1890
-							}    // Cap $monthday on month length (eg 28 feb instead of 29 feb)
1891
-							$daynow = $monthstart + ($monthday - 1) * 24 * 60 * 60;
1892
-							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1893
-						}
1894
-						elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] in month X of every N years
1895
-							// Go the correct month
1896
-							$monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60;
1897
-
1898
-							// Find first matching weekday in this month
1899
-							for ($wday = 0; $wday < 7; ++$wday) {
1900
-								$daynow = $monthnow + $wday * 60 * 60 * 24;
1901
-								$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1902
-
1903
-								if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1904
-									$firstday = $wday;
1905
-
1906
-									break;
1907
-								}
1908
-							}
1909
-
1910
-							// Same as above (monthly)
1911
-							$daynow = $monthnow + ($firstday + ($this->recur["nday"] - 1) * 7) * 60 * 60 * 24;
1912
-
1913
-							while ($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) {
1914
-								$daynow -= 7 * 60 * 60 * 24;
1915
-							}
1916
-
1917
-							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1918
-						}
1919
-						elseif ($this->recur['regen']) {
1920
-							$year_starttime = $this->gmtime($now);
1921
-							$is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1);    // +1 next year
1922
-							$now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /* year in seconds */);
1923
-
1924
-							if ($now <= $dayend) {
1925
-								$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1926
-							}
1927
-						}
1928
-					}
1929
-				}
1930
-				// to get all exception items
1931
-				if (!empty($this->recur['changed_occurrences'])) {
1932
-					$this->processExceptionItems($items, $start, $end);
1933
-				}
1934
-			}
1935
-
1936
-			// sort items on starttime
1937
-			usort($items, [$this, "sortStarttime"]);
1938
-
1939
-			// Return the MAPI-compatible list of items for this object
1940
-			return $items;
1941
-		}
1942
-
1943
-		public function sortStarttime($a, $b) {
1944
-			$aTime = $a[$this->proptags["startdate"]];
1945
-			$bTime = $b[$this->proptags["startdate"]];
1946
-
1947
-			return $aTime == $bTime ? 0 : ($aTime > $bTime ? 1 : -1);
1948
-		}
1949
-
1950
-		/**
1951
-		 * daysInMonth.
1952
-		 *
1953
-		 * Returns the number of days in the upcoming number of months. If you specify 1 month as
1954
-		 * $months it will give you the number of days in the month of $date. If you specify more it
1955
-		 * will also count the days in the upcoming months and add that to the number of days. So
1956
-		 * if you have a date in march and you specify $months as 2 it will return 61.
1957
-		 *
1958
-		 * @param int $date   specified date as timestamp from which you want to know the number
1959
-		 *                    of days in the month
1960
-		 * @param int $months number of months you want to know the number of days in
1961
-		 * @returns Integer Number of days in the specified amount of months.
1962
-		 */
1963
-		public function daysInMonth($date, $months) {
1964
-			$days = 0;
1965
-
1966
-			for ($i = 0; $i < $months; ++$i) {
1967
-				$days += date("t", $date + $days * 24 * 60 * 60);
1968
-			}
1969
-
1970
-			return $days;
1971
-		}
1972
-
1973
-		// Converts MAPI-style 'minutes' into the month of the year [0..11]
1974
-		public function monthOfYear($minutes) {
1975
-			$d = gmmktime(0, 0, 0, 1, 1, 2001); // The year 2001 was a non-leap year, and the minutes provided are always in non-leap-year-minutes
1976
-
1977
-			$d += $minutes * 60;
1978
-
1979
-			$dtime = $this->gmtime($d);
1980
-
1981
-			return $dtime["tm_mon"];
1982
-		}
1983
-
1984
-		public function sortExceptionStart($a, $b) {
1985
-			return $a["start"] == $b["start"] ? 0 : ($a["start"] > $b["start"] ? 1 : -1);
1986
-		}
1987
-	}
1860
+                            if ($daynow <= $dayend && $daynow >= $daystart) {
1861
+                                $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1862
+                            }
1863
+                        }
1864
+                        elseif ($this->recur['regen']) {
1865
+                            $next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60);
1866
+                            $now = $daystart + ($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60);
1867
+
1868
+                            if ($now <= $dayend) {
1869
+                                $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1870
+                            }
1871
+                        }
1872
+                    }
1873
+
1874
+                    break;
1875
+
1876
+                case 13:
1877
+                    // Yearly
1878
+                    if ($this->recur["everyn"] <= 0) {
1879
+                        $this->recur["everyn"] = 12;
1880
+                    }
1881
+
1882
+                    for ($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60) {
1883
+                        if (isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month
1884
+                            // recur["month"] is in minutes since the beginning of the year
1885
+                            $month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11]
1886
+                            $monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31]
1887
+                            $monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month
1888
+                            if ($monthday > $this->daysInMonth($monthstart, 1)) {
1889
+                                $monthday = $this->daysInMonth($monthstart, 1);
1890
+                            }    // Cap $monthday on month length (eg 28 feb instead of 29 feb)
1891
+                            $daynow = $monthstart + ($monthday - 1) * 24 * 60 * 60;
1892
+                            $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1893
+                        }
1894
+                        elseif (isset($this->recur["nday"], $this->recur["weekdays"])) { // Nth [weekday] in month X of every N years
1895
+                            // Go the correct month
1896
+                            $monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60;
1897
+
1898
+                            // Find first matching weekday in this month
1899
+                            for ($wday = 0; $wday < 7; ++$wday) {
1900
+                                $daynow = $monthnow + $wday * 60 * 60 * 24;
1901
+                                $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1902
+
1903
+                                if ($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1904
+                                    $firstday = $wday;
1905
+
1906
+                                    break;
1907
+                                }
1908
+                            }
1909
+
1910
+                            // Same as above (monthly)
1911
+                            $daynow = $monthnow + ($firstday + ($this->recur["nday"] - 1) * 7) * 60 * 60 * 24;
1912
+
1913
+                            while ($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) {
1914
+                                $daynow -= 7 * 60 * 60 * 24;
1915
+                            }
1916
+
1917
+                            $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1918
+                        }
1919
+                        elseif ($this->recur['regen']) {
1920
+                            $year_starttime = $this->gmtime($now);
1921
+                            $is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1);    // +1 next year
1922
+                            $now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /* year in seconds */);
1923
+
1924
+                            if ($now <= $dayend) {
1925
+                                $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1926
+                            }
1927
+                        }
1928
+                    }
1929
+                }
1930
+                // to get all exception items
1931
+                if (!empty($this->recur['changed_occurrences'])) {
1932
+                    $this->processExceptionItems($items, $start, $end);
1933
+                }
1934
+            }
1935
+
1936
+            // sort items on starttime
1937
+            usort($items, [$this, "sortStarttime"]);
1938
+
1939
+            // Return the MAPI-compatible list of items for this object
1940
+            return $items;
1941
+        }
1942
+
1943
+        public function sortStarttime($a, $b) {
1944
+            $aTime = $a[$this->proptags["startdate"]];
1945
+            $bTime = $b[$this->proptags["startdate"]];
1946
+
1947
+            return $aTime == $bTime ? 0 : ($aTime > $bTime ? 1 : -1);
1948
+        }
1949
+
1950
+        /**
1951
+         * daysInMonth.
1952
+         *
1953
+         * Returns the number of days in the upcoming number of months. If you specify 1 month as
1954
+         * $months it will give you the number of days in the month of $date. If you specify more it
1955
+         * will also count the days in the upcoming months and add that to the number of days. So
1956
+         * if you have a date in march and you specify $months as 2 it will return 61.
1957
+         *
1958
+         * @param int $date   specified date as timestamp from which you want to know the number
1959
+         *                    of days in the month
1960
+         * @param int $months number of months you want to know the number of days in
1961
+         * @returns Integer Number of days in the specified amount of months.
1962
+         */
1963
+        public function daysInMonth($date, $months) {
1964
+            $days = 0;
1965
+
1966
+            for ($i = 0; $i < $months; ++$i) {
1967
+                $days += date("t", $date + $days * 24 * 60 * 60);
1968
+            }
1969
+
1970
+            return $days;
1971
+        }
1972
+
1973
+        // Converts MAPI-style 'minutes' into the month of the year [0..11]
1974
+        public function monthOfYear($minutes) {
1975
+            $d = gmmktime(0, 0, 0, 1, 1, 2001); // The year 2001 was a non-leap year, and the minutes provided are always in non-leap-year-minutes
1976
+
1977
+            $d += $minutes * 60;
1978
+
1979
+            $dtime = $this->gmtime($d);
1980
+
1981
+            return $dtime["tm_mon"];
1982
+        }
1983
+
1984
+        public function sortExceptionStart($a, $b) {
1985
+            return $a["start"] == $b["start"] ? 0 : ($a["start"] > $b["start"] ? 1 : -1);
1986
+        }
1987
+    }
Please login to merge, or discard this patch.
mapi/class.baseexception.php 1 patch
Indentation   +125 added lines, -125 removed lines patch added patch discarded remove patch
@@ -17,129 +17,129 @@
 block discarded – undo
17 17
  * getTraceAsString() - formated string of trace
18 18
  */
19 19
 class BaseException extends Exception {
20
-	/**
21
-	 * Base name of the file, so we don't have to use static path of the file.
22
-	 */
23
-	private $baseFile;
24
-
25
-	/**
26
-	 * Flag to check if exception is already handled or not.
27
-	 */
28
-	public $isHandled = false;
29
-
30
-	/**
31
-	 * The exception message to show at client side.
32
-	 */
33
-	public $displayMessage;
34
-
35
-	/**
36
-	 * Flag for allow to exception details message or not.
37
-	 */
38
-	public $allowToShowDetailsMessage = false;
39
-
40
-	/**
41
-	 * The exception title to show as a message box title at client side.
42
-	 */
43
-	public $title;
44
-
45
-	/**
46
-	 * Construct the exception.
47
-	 *
48
-	 * @param string    $errorMessage
49
-	 * @param int       $code
50
-	 * @param Exception $previous
51
-	 * @param string    $displayMessage
52
-	 */
53
-	public function __construct($errorMessage, $code = 0, Exception $previous = null, $displayMessage = null) {
54
-		// assign display message
55
-		$this->displayMessage = $displayMessage;
56
-
57
-		parent::__construct($errorMessage, (int) $code, $previous);
58
-	}
59
-
60
-	/**
61
-	 * @return string returns file name and line number combined where exception occurred
62
-	 */
63
-	public function getFileLine() {
64
-		return $this->getBaseFile() . ':' . $this->getLine();
65
-	}
66
-
67
-	/**
68
-	 * @return string returns message that should be sent to client to display
69
-	 */
70
-	public function getDisplayMessage() {
71
-		if (!is_null($this->displayMessage)) {
72
-			return $this->displayMessage;
73
-		}
74
-
75
-		return $this->getMessage();
76
-	}
77
-
78
-	/**
79
-	 * Function sets display message of an exception that will be sent to the client side
80
-	 * to show it to user.
81
-	 *
82
-	 * @param string $message display message
83
-	 */
84
-	public function setDisplayMessage($message) {
85
-		$this->displayMessage = $message;
86
-	}
87
-
88
-	/**
89
-	 * Function sets title of an exception that will be sent to the client side
90
-	 * to show it to user.
91
-	 *
92
-	 * @param string $title title of an exception
93
-	 */
94
-	public function setTitle($title) {
95
-		$this->title = $title;
96
-	}
97
-
98
-	/**
99
-	 * @return string returns title that should be sent to client to display as a message box
100
-	 *                title
101
-	 */
102
-	public function getTitle() {
103
-		return $this->title;
104
-	}
105
-
106
-	/**
107
-	 * Function sets a flag in exception class to indicate that exception is already handled
108
-	 * so if it is caught again in the top level of function stack then we have to silently
109
-	 * ignore it.
110
-	 */
111
-	public function setHandled() {
112
-		$this->isHandled = true;
113
-	}
114
-
115
-	/**
116
-	 * @return string returns base path of the file where exception occurred
117
-	 */
118
-	public function getBaseFile() {
119
-		if (is_null($this->baseFile)) {
120
-			$this->baseFile = basename(parent::getFile());
121
-		}
122
-
123
-		return $this->baseFile;
124
-	}
125
-
126
-	/**
127
-	 * Name of the class of exception.
128
-	 *
129
-	 * @return string
130
-	 */
131
-	public function getName() {
132
-		return get_class($this);
133
-	}
134
-
135
-	/**
136
-	 * It will return details error message if allowToShowDetailsMessage is set.
137
-	 *
138
-	 * @return string returns details error message
139
-	 */
140
-	public function getDetailsMessage() {
141
-		return $this->allowToShowDetailsMessage ? $this->__toString() : '';
142
-	}
143
-
144
-	// @TODO getTrace and getTraceAsString
20
+    /**
21
+     * Base name of the file, so we don't have to use static path of the file.
22
+     */
23
+    private $baseFile;
24
+
25
+    /**
26
+     * Flag to check if exception is already handled or not.
27
+     */
28
+    public $isHandled = false;
29
+
30
+    /**
31
+     * The exception message to show at client side.
32
+     */
33
+    public $displayMessage;
34
+
35
+    /**
36
+     * Flag for allow to exception details message or not.
37
+     */
38
+    public $allowToShowDetailsMessage = false;
39
+
40
+    /**
41
+     * The exception title to show as a message box title at client side.
42
+     */
43
+    public $title;
44
+
45
+    /**
46
+     * Construct the exception.
47
+     *
48
+     * @param string    $errorMessage
49
+     * @param int       $code
50
+     * @param Exception $previous
51
+     * @param string    $displayMessage
52
+     */
53
+    public function __construct($errorMessage, $code = 0, Exception $previous = null, $displayMessage = null) {
54
+        // assign display message
55
+        $this->displayMessage = $displayMessage;
56
+
57
+        parent::__construct($errorMessage, (int) $code, $previous);
58
+    }
59
+
60
+    /**
61
+     * @return string returns file name and line number combined where exception occurred
62
+     */
63
+    public function getFileLine() {
64
+        return $this->getBaseFile() . ':' . $this->getLine();
65
+    }
66
+
67
+    /**
68
+     * @return string returns message that should be sent to client to display
69
+     */
70
+    public function getDisplayMessage() {
71
+        if (!is_null($this->displayMessage)) {
72
+            return $this->displayMessage;
73
+        }
74
+
75
+        return $this->getMessage();
76
+    }
77
+
78
+    /**
79
+     * Function sets display message of an exception that will be sent to the client side
80
+     * to show it to user.
81
+     *
82
+     * @param string $message display message
83
+     */
84
+    public function setDisplayMessage($message) {
85
+        $this->displayMessage = $message;
86
+    }
87
+
88
+    /**
89
+     * Function sets title of an exception that will be sent to the client side
90
+     * to show it to user.
91
+     *
92
+     * @param string $title title of an exception
93
+     */
94
+    public function setTitle($title) {
95
+        $this->title = $title;
96
+    }
97
+
98
+    /**
99
+     * @return string returns title that should be sent to client to display as a message box
100
+     *                title
101
+     */
102
+    public function getTitle() {
103
+        return $this->title;
104
+    }
105
+
106
+    /**
107
+     * Function sets a flag in exception class to indicate that exception is already handled
108
+     * so if it is caught again in the top level of function stack then we have to silently
109
+     * ignore it.
110
+     */
111
+    public function setHandled() {
112
+        $this->isHandled = true;
113
+    }
114
+
115
+    /**
116
+     * @return string returns base path of the file where exception occurred
117
+     */
118
+    public function getBaseFile() {
119
+        if (is_null($this->baseFile)) {
120
+            $this->baseFile = basename(parent::getFile());
121
+        }
122
+
123
+        return $this->baseFile;
124
+    }
125
+
126
+    /**
127
+     * Name of the class of exception.
128
+     *
129
+     * @return string
130
+     */
131
+    public function getName() {
132
+        return get_class($this);
133
+    }
134
+
135
+    /**
136
+     * It will return details error message if allowToShowDetailsMessage is set.
137
+     *
138
+     * @return string returns details error message
139
+     */
140
+    public function getDetailsMessage() {
141
+        return $this->allowToShowDetailsMessage ? $this->__toString() : '';
142
+    }
143
+
144
+    // @TODO getTrace and getTraceAsString
145 145
 }
Please login to merge, or discard this patch.