Issues (207)

class.recurrence.php (12 issues)

1
<?php
2
/*
3
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH
6
 */
7
8
/**
9
 * Recurrence.
10
 */
11
class Recurrence extends BaseRecurrence {
12
	/*
13
	 * ABOUT TIMEZONES
14
	 *
15
	 * Timezones are rather complicated here so here are some rules to think about:
16
	 *
17
	 * - Timestamps in mapi-like properties (so in PT_SYSTIME properties) are always in GMT (including
18
	 *   the 'basedate' property in exceptions !!)
19
	 * - Timestamps for recurrence (so start/end of recurrence, and basedates for exceptions (everything
20
	 *   outside the 'basedate' property in the exception !!), and start/endtimes for exceptions) are
21
	 *   always in LOCAL time.
22
	 */
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 mixed    $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 (!empty($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:" . PidLidAppointmentStartWhole;
66
			$properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidAppointmentEndWhole;
67
			$properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:" . PidLidRecurring;
68
			$properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:" . PidLidAppointmentRecur;
69
			$properties["busystatus"] = "PT_LONG:PSETID_Appointment:" . PidLidBusyStatus;
70
			$properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
71
			$properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:" . PidLidAppointmentSubType;
72
			$properties["private"] = "PT_BOOLEAN:PSETID_Common:" . PidLidPrivate;
73
			$properties["meeting"] = "PT_LONG:PSETID_Appointment:" . PidLidAppointmentStateFlags;
74
			$properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidClipStart;
75
			$properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidClipEnd;
76
			$properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:" . PidLidRecurrencePattern;
77
			$properties["location"] = "PT_STRING8:PSETID_Appointment:" . PidLidLocation;
78
			$properties["duration"] = "PT_LONG:PSETID_Appointment:" . PidLidAppointmentDuration;
79
			$properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
80
			$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:" . PidLidReminderSet;
81
			$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:" . PidLidReminderDelta;
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:" . PidLidReminderTime;
87
			$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
88
			$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
89
			$properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidExceptionReplaceTime;
90
			$properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:" . PidLidTimeZoneStruct;
91
			$properties["timezone"] = "PT_STRING8:PSETID_Appointment:" . PidLidTimeZoneDescription;
92
			$properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:" . PidLidReminderSignalTime;
93
			$properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
94
			$properties["hideattachments"] = "PT_BOOLEAN:PSETID_Common:" . PidLidSmartNoAttach;
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 mixed $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 list of recipients
109
	 * @param mixed $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): bool {
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);
0 ignored issues
show
It seems like $copy_attach_from can also be of type false; however, parameter $copy_attach_from of Recurrence::createExceptionAttachment() does only seem to accept mapi_message, maybe add an additional type check? ( Ignorable by Annotation )

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

157
			$this->createExceptionAttachment($props, $exception_recips, /** @scrutinizer ignore-type */ $copy_attach_from);
Loading history...
158
159
			$changed_item = [];
160
161
			$changed_item["basedate"] = $basetime;
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): bool {
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) {
0 ignored issues
show
$extomodify is of type mixed, thus it always evaluated to false.
Loading history...
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): bool {
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
	 *
374
	 * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE)
375
	 */
376
	public function isValidReminderTime($basedate, $reminderminutes, $startdate): bool {
377
		// get all occurrence items before the selected items occurrence starttime
378
		$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate));
0 ignored issues
show
It seems like $this->toGMT($this->tz, $basedate) can also be of type date; however, parameter $end of BaseRecurrence::getItems() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

378
		$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], /** @scrutinizer ignore-type */ $this->toGMT($this->tz, $basedate));
Loading history...
379
380
		if (!empty($occitems)) {
381
			// as occitems array is sorted in ascending order of startdate, to get the previous occurrence we take the last items in occitems .
382
			$previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]];
383
384
			// if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed
385
			if ($startdate - ($reminderminutes * 60) <= $previousitem_startdate) {
386
				return false;
387
			}
388
		}
389
390
		// Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence)
391
		$currentOcc = $this->getItems($this->toGMT($this->tz, $basedate), 0x7FF00000, 2, true);
0 ignored issues
show
It seems like $this->toGMT($this->tz, $basedate) can also be of type date; however, parameter $start of BaseRecurrence::getItems() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

391
		$currentOcc = $this->getItems(/** @scrutinizer ignore-type */ $this->toGMT($this->tz, $basedate), 0x7FF00000, 2, true);
Loading history...
392
393
		// If there are another two occurrences, then the first is the current occurrence, and the one after that
394
		// is the next occurrence.
395
		if (count($currentOcc) > 1) {
396
			$next = $currentOcc[1];
397
			// Get reminder time of the next occurrence.
398
			$nextOccReminderTime = $next[$this->proptags["startdate"]] - ($next[$this->proptags["reminder_minutes"]] * 60);
399
			// If the reminder time of the next item is before the start of this item, then that's not allowed
400
			if ($nextOccReminderTime <= $startdate) {
401
				return false;
402
			}
403
		}
404
405
		// All was ok
406
		return true;
407
	}
408
409
	public function setRecurrence($tz, $recur): void {
410
		// only reset timezone if specified
411
		if ($tz) {
412
			$this->tz = $tz;
413
		}
414
415
		$this->recur = $recur;
416
417
		if (!isset($this->recur["changed_occurrences"])) {
418
			$this->recur["changed_occurrences"] = [];
419
		}
420
421
		if (!isset($this->recur["deleted_occurrences"])) {
422
			$this->recur["deleted_occurrences"] = [];
423
		}
424
425
		$this->deleteAttachments();
426
		$this->saveRecurrence();
427
428
		// if client has not set the recurring_pattern then we should generate it and save it
429
		$messageProps = mapi_getprops($this->message, [$this->proptags["recurring_pattern"]]);
430
		if (empty($messageProps[$this->proptags["recurring_pattern"]])) {
431
			$this->saveRecurrencePattern();
432
		}
433
	}
434
435
	// Returns the start or end time of the occurrence on the given base date.
436
	// This assumes that the basedate you supply is in LOCAL time
437
	public function getOccurrenceStart($basedate) {
438
		$daystart = $this->dayStartOf($basedate);
439
440
		return $this->toGMT($this->tz, $daystart + $this->recur["startocc"] * 60);
441
	}
442
443
	public function getOccurrenceEnd($basedate) {
444
		$daystart = $this->dayStartOf($basedate);
445
446
		return $this->toGMT($this->tz, $daystart + $this->recur["endocc"] * 60);
447
	}
448
449
	/**
450
	 * This function returns the next remindertime starting from $timestamp
451
	 * When no next reminder exists, false is returned.
452
	 *
453
	 * Note: Before saving this new reminder time (when snoozing), you must check for
454
	 *       yourself if this reminder time is earlier than your snooze time, else
455
	 *       use your snooze time and not this reminder time.
456
	 *
457
	 * @param mixed $timestamp
458
	 */
459
	public function getNextReminderTime($timestamp) {
460
		/**
461
		 * Get next item from now until forever, but max 1 item with reminder set
462
		 * Note 0x7ff00000 instead of 0x7fffffff because of possible overflow failures when converting to GMT....
463
		 * Here for getting next 10 occurrences assuming that next here we will be able to find
464
		 * nextreminder occurrence in 10 occurrences.
465
		 */
466
		$items = $this->getItems($timestamp, 0x7FF00000, 10, true);
467
468
		// Initially setting nextreminder to false so when no next reminder exists, false is returned.
469
		$nextreminder = false;
470
		/*
471
		 * Loop through all reminder which we get in items variable
472
		 * and check whether the remindertime is greater than timestamp.
473
		 * On the first occurrence of greater nextreminder break the loop
474
		 * and return the value to calling function.
475
		 */
476
		for ($i = 0, $len = count($items); $i < $len; ++$i) {
477
			$item = $items[$i];
478
			$tempnextreminder = $item[$this->proptags["startdate"]] - ($item[$this->proptags["reminder_minutes"]] * 60);
479
480
			// If tempnextreminder is greater than timestamp then save it in nextreminder and break from the loop.
481
			if ($tempnextreminder > $timestamp) {
482
				$nextreminder = $tempnextreminder;
483
				break;
484
			}
485
		}
486
487
		return $nextreminder;
488
	}
489
490
	/**
491
	 * Note: Static function, more like a utility function.
492
	 *
493
	 * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are
494
	 * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval
495
	 * 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
496
	 * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>.
497
	 *
498
	 * @param $store          resource The store in which the calendar resides
499
	 * @param $calendar       resource The calendar to get the items from
500
	 * @param $viewstart      int Timestamp of beginning of view window
501
	 * @param $viewend        int Timestamp of end of view window
502
	 * @param $propsrequested array Array of properties to return
503
	 *
504
	 * @psalm-param list{0: mixed, 1: mixed, 2?: mixed} $propsrequested
505
	 */
506
	public static function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested) {
507
		return getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested);
508
	}
509
510
	/*
511
	 * CODE BELOW THIS LINE IS FOR INTERNAL USE ONLY
512
	 *****************************************************************************************************************
513
	 */
514
515
	/**
516
	 * Returns langified daily recurrence type string, whether it's singular or plural,
517
	 * recurrence interval.
518
	 */
519
	public function getI18RecTypeDaily(mixed $type, mixed $interval, bool $occSingleDayRank): array {
520
		switch ($interval) {
521
			case 1: // workdays
522
				$type = dgettext('zarafa', 'workday');
523
				$occSingleDayRank = true;
524
				break;
525
526
			case 1440: // daily
527
				$type = dgettext('zarafa', 'day');
528
				$occSingleDayRank = true;
529
				break;
530
531
			default: // every $interval days
532
				$interval /= 1440;
533
				$type = dgettext('zarafa', 'days');
534
				$occSingleDayRank = false;
535
				break;
536
		}
537
538
		return [
539
			'type' => $type,
540
			'interval' => $interval,
541
			'occSingleDayRank' => boolval($occSingleDayRank),
542
		];
543
	}
544
545
	/**
546
	 * Returns langified weekly recurrence type string, whether it's singular or plural,
547
	 * recurrence interval.
548
	 */
549
	public function getI18RecTypeWeekly(mixed $type, mixed $interval, bool $occSingleDayRank): array {
550
		if ($interval == 1) {
551
			$type = dgettext('zarafa', 'week');
552
			$occSingleDayRank = true;
553
		}
554
		else {
555
			$type = dgettext('zarafa', 'weeks');
556
			$occSingleDayRank = false;
557
		}
558
		$daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
559
		$type .= sprintf(" %s ", dgettext('zarafa', 'on'));
560
561
		for ($j = 0, $weekdays = (int) $this->recur["weekdays"]; $j < 7; ++$j) {
562
			if ($weekdays & (1 << $j)) {
563
				$type .= sprintf("%s, ", dgettext('zarafa', $daysOfWeek[$j]));
564
			}
565
		}
566
		$type = trim($type, ", ");
567
		if (($pos = strrpos($type, ",")) !== false) {
568
			$type = substr_replace($type, " " . dgettext('zarafa', 'and'), $pos, 1);
569
		}
570
571
		return [
572
			'type' => $type,
573
			'interval' => $interval,
574
			'occSingleDayRank' => boolval($occSingleDayRank),
575
		];
576
	}
577
578
	/**
579
	 * Returns langified monthly recurrence type string, whether it's singular or plural,
580
	 * recurrence interval.
581
	 */
582
	public function getI18RecTypeMonthly(mixed $type, mixed $interval, bool $occSingleDayRank): array {
583
		if ($interval == 1) {
584
			$type = dgettext('zarafa', 'month');
585
			$occSingleDayRank = true;
586
		}
587
		else {
588
			$type = dgettext('zarafa', 'months');
589
			$occSingleDayRank = false;
590
		}
591
592
		return [
593
			'type' => $type,
594
			'interval' => $interval,
595
			'occSingleDayRank' => boolval($occSingleDayRank),
596
		];
597
	}
598
599
	/**
600
	 * Returns langified yearly recurrence type string, whether it's singular or plural,
601
	 * recurrence interval.
602
	 */
603
	public function getI18RecTypeYearly(mixed $type, mixed $interval, bool $occSingleDayRank): array {
604
		if ($interval <= 12) {
605
			$interval = 1;
606
			$type = dgettext('zarafa', 'year');
607
			$occSingleDayRank = true;
608
		}
609
		else {
610
			$interval = $interval / 12;
611
			$type = dgettext('zarafa', 'years');
612
			$occSingleDayRank = false;
613
		}
614
615
		return [
616
			'type' => $type,
617
			'interval' => $interval,
618
			'occSingleDayRank' => boolval($occSingleDayRank),
619
		];
620
	}
621
622
	/**
623
	 * Returns langified recurrence type string, whether it's singular or plural,
624
	 * recurrence interval.
625
	 */
626
	public function getI18nRecurrenceType(): array {
627
		$type = $this->recur['type'];
628
		$interval = $this->recur['everyn'];
629
		$occSingleDayRank = false;
630
631
		switch ($type) {
632
			case 0x0A: // Daily
633
				return $this->getI18RecTypeDaily($type, $interval, $occSingleDayRank);
634
635
			case 0x0B: // Weekly
636
				return $this->getI18RecTypeWeekly($type, $interval, $occSingleDayRank);
637
638
			case 0x0C: // Monthly
639
				return $this->getI18RecTypeMonthly($type, $interval, $occSingleDayRank);
640
641
			case 0x0D: // Yearly
642
				return $this->getI18RecTypeYearly($type, $interval, $occSingleDayRank);
643
		}
644
645
		return [
646
			'type' => $type,
647
			'interval' => $interval,
648
			'occSingleDayRank' => boolval($occSingleDayRank),
649
		];
650
	}
651
652
	/**
653
	 * Returns the start or end time of the first occurrence.
654
	 */
655
	public function getOccDate(bool $getStart = true): mixed {
656
		return $getStart ?
657
			(isset($this->recur['startocc']) ?
658
				$this->recur['start'] + (((int) $this->recur['startocc']) * 60) :
659
				$this->recur['start']) :
660
			(isset($this->recur['endocc']) ?
661
				$this->recur['start'] + (((int) $this->recur['endocc']) * 60) :
662
				$this->recur['end']);
663
	}
664
665
	/**
666
	 * Returns langified occurrence time.
667
	 */
668
	public function getI18nTime(string $format, mixed $occTime): string {
669
		return gmdate(dgettext('zarafa', $format), $occTime);
670
	}
671
672
	/**
673
	 * Returns langified recurrence pattern termination after the given date.
674
	 */
675
	public function getI18nRecTermDate(
676
		bool $occTimeRange,
677
		bool $occSingleDayRank,
678
		mixed $type,
679
		mixed $interval,
680
		string $start,
681
		string $end,
682
		string $startocc,
683
		string $endocc
684
	): string {
685
		return $occTimeRange ?
686
			(
687
				$occSingleDayRank ?
688
					sprintf(
689
						dgettext('zarafa', 'Occurs every %s effective %s until %s from %s to %s.'),
690
						$type,
691
						$start,
692
						$end,
693
						$startocc,
694
						$endocc
695
					) :
696
					sprintf(
697
						dgettext('zarafa', 'Occurs every %s %s effective %s until %s from %s to %s.'),
698
						$interval,
699
						$type,
700
						$start,
701
						$end,
702
						$startocc,
703
						$endocc
704
					)
705
			) :
706
			(
707
				$occSingleDayRank ?
708
					sprintf(
709
						dgettext('zarafa', 'Occurs every %s effective %s until %s.'),
710
						$type,
711
						$start,
712
						$end
713
					) :
714
					sprintf(
715
						dgettext('zarafa', 'Occurs every %s %s effective %s until %s.'),
716
						$interval,
717
						$type,
718
						$start,
719
						$end
720
					)
721
			);
722
	}
723
724
	/**
725
	 * Returns langified recurrence pattern termination after a number of
726
	 * occurrences.
727
	 */
728
	public function getI18nRecTermNrOcc(
729
		bool $occTimeRange,
730
		bool $occSingleDayRank,
731
		mixed $type,
732
		mixed $interval,
733
		string $start,
734
		mixed $numocc,
735
		string $startocc,
736
		string $endocc
737
	): string {
738
		return $occTimeRange ?
739
			(
740
				$occSingleDayRank ?
741
					sprintf(
742
						dngettext(
743
							'zarafa',
744
							'Occurs every %s effective %s for %s occurrence from %s to %s.',
745
							'Occurs every %s effective %s for %s occurrences from %s to %s.',
746
							$numocc
747
						),
748
						$type,
749
						$start,
750
						$numocc,
751
						$startocc,
752
						$endocc
753
					) :
754
					sprintf(
755
						dngettext(
756
							'zarafa',
757
							'Occurs every %s %s effective %s for %s occurrence from %s to %s.',
758
							'Occurs every %s %s effective %s for %s occurrences %s to %s.',
759
							$numocc
760
						),
761
						$interval,
762
						$type,
763
						$start,
764
						$numocc,
765
						$startocc,
766
						$endocc
767
					)
768
			) :
769
			(
770
				$occSingleDayRank ?
771
					sprintf(
772
						dngettext(
773
							'zarafa',
774
							'Occurs every %s effective %s for %s occurrence.',
775
							'Occurs every %s effective %s for %s occurrences.',
776
							$numocc
777
						),
778
						$type,
779
						$start,
780
						$numocc
781
					) :
782
					sprintf(
783
						dngettext(
784
							'zarafa',
785
							'Occurs every %s %s effective %s for %s occurrence.',
786
							'Occurs every %s %s effective %s for %s occurrences.',
787
							$numocc
788
						),
789
						$interval,
790
						$type,
791
						$start,
792
						$numocc
793
					)
794
			);
795
	}
796
797
	/**
798
	 * Returns langified recurrence pattern termination with no end date.
799
	 */
800
	public function getI18nRecTermNoEnd(
801
		bool $occTimeRange,
802
		bool $occSingleDayRank,
803
		mixed $type,
804
		mixed $interval,
805
		string $start,
806
		string $startocc,
807
		string $endocc
808
	): string {
809
		return $occTimeRange ?
810
			(
811
				$occSingleDayRank ?
812
					sprintf(
813
						dgettext('zarafa', 'Occurs every %s effective %s from %s to %s.'),
814
						$type,
815
						$start,
816
						$startocc,
817
						$endocc
818
					) :
819
					sprintf(
820
						dgettext('zarafa', 'Occurs every %s %s effective %s from %s to %s.'),
821
						$interval,
822
						$type,
823
						$start,
824
						$startocc,
825
						$endocc
826
					)
827
			) :
828
			(
829
				$occSingleDayRank ?
830
					sprintf(
831
						dgettext('zarafa', 'Occurs every %s effective %s.'),
832
						$type,
833
						$start
834
					) :
835
					sprintf(
836
						dgettext('zarafa', 'Occurs every %s %s effective %s.'),
837
						$interval,
838
						$type,
839
						$start
840
					)
841
			);
842
	}
843
844
	/**
845
	 * Generates and stores recurrence pattern string to recurring_pattern property.
846
	 */
847
	public function saveRecurrencePattern(): string {
848
		// Start formatting the properties in such a way we can apply
849
		// them directly into the recurrence pattern.
850
		$pattern = '';
851
		$occTimeRange = $this->recur['startocc'] != 0 && $this->recur['endocc'] != 0;
852
853
		[
854
			'type' => $type,
855
			'interval' => $interval,
856
			'occSingleDayRank' => $occSingleDayRank
857
		] = $this->getI18nRecurrenceType();
858
859
		// get timings of the first occurrence
860
		$firstoccstartdate = $this->getOccDate();
861
		$firstoccenddate = $this->getOccDate(false);
862
863
		$start = $this->getI18nTime('d-m-Y', $firstoccstartdate);
864
		$end = $this->getI18nTime('d-m-Y', $firstoccenddate);
865
		$startocc = $this->getI18nTime('G:i', $firstoccstartdate);
866
		$endocc = $this->getI18nTime('G:i', $firstoccenddate);
867
868
		// Based on the properties, we need to generate the recurrence pattern string.
869
		// This is obviously very easy since we can simply concatenate a bunch of strings,
870
		// however this messes up translations for languages which order their words
871
		// differently.
872
		// To improve translation quality we create a series of default strings, in which
873
		// we only have to fill in the correct variables. The base string is thus selected
874
		// based on the available properties.
875
		switch ($this->recur['term']) {
876
			case 0x21: // After the given enddate
877
				$pattern = $this->getI18nRecTermDate(
878
					$occTimeRange,
879
					boolval($occSingleDayRank),
880
					$type,
881
					$interval,
882
					$start,
883
					$end,
884
					$startocc,
885
					$endocc
886
				);
887
				break;
888
889
			case 0x22: // After a number of times
890
				$pattern = $this->getI18nRecTermNrOcc(
891
					$occTimeRange,
892
					boolval($occSingleDayRank),
893
					$type,
894
					$interval,
895
					$start,
896
					$this->recur['numoccur'] ?? 0,
897
					$startocc,
898
					$endocc
899
				);
900
				break;
901
902
			case 0x23: // Never ends
903
				$pattern = $this->getI18nRecTermNoEnd(
904
					$occTimeRange,
905
					boolval($occSingleDayRank),
906
					$type,
907
					$interval,
908
					$start,
909
					$startocc,
910
					$endocc
911
				);
912
				break;
913
914
			default:
915
				error_log(sprintf("Invalid recurrence pattern termination %d", $this->recur['term']));
916
				break;
917
		}
918
919
		if (!empty($pattern)) {
920
			mapi_setprops($this->message, [$this->proptags["recurring_pattern"] => $pattern]);
921
		}
922
923
		return $pattern;
924
	}
925
926
	/*
927
	 * Remove an exception by base_date. This is the base date in local daystart time
928
	 */
929
	/**
930
	 * @param false|int $base_date
931
	 */
932
	public function deleteException($base_date): void {
933
		// Remove all exceptions on $base_date from the deleted and changed occurrences lists
934
935
		// Remove all items in $todelete from deleted_occurrences
936
		$new = [];
937
938
		foreach ($this->recur["deleted_occurrences"] as $entry) {
939
			if ($entry != $base_date) {
940
				$new[] = $entry;
941
			}
942
		}
943
		$this->recur["deleted_occurrences"] = $new;
944
945
		$new = [];
946
947
		foreach ($this->recur["changed_occurrences"] as $entry) {
948
			if (!$this->isSameDay($entry["basedate"], $base_date)) {
949
				$new[] = $entry;
950
			}
951
			else {
952
				$this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
953
			}
954
		}
955
956
		$this->recur["changed_occurrences"] = $new;
957
	}
958
959
	/**
960
	 * Function which saves the exception data in an attachment.
961
	 *
962
	 * @param array        $exception_props  the exception data (like any other MAPI appointment)
963
	 * @param array        $exception_recips list of recipients
964
	 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
0 ignored issues
show
The type mapi_message was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
965
	 */
966
	public function createExceptionAttachment($exception_props, $exception_recips = [], $copy_attach_from = false): void {
967
		// Create new attachment.
968
		$attachment = mapi_message_createattach($this->message);
969
		$props = [];
970
		$props[PR_ATTACHMENT_FLAGS] = 2;
971
		$props[PR_ATTACHMENT_HIDDEN] = true;
972
		$props[PR_ATTACHMENT_LINKID] = 0;
973
		$props[PR_ATTACH_FLAGS] = 0;
974
		$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
975
		$props[PR_DISPLAY_NAME] = "Exception";
976
		$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
977
		$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
978
		mapi_setprops($attachment, $props);
979
980
		$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
981
982
		if ($copy_attach_from) {
983
			$attachmentTable = mapi_message_getattachmenttable($copy_attach_from);
984
			if ($attachmentTable) {
0 ignored issues
show
$attachmentTable is of type resource, thus it always evaluated to true.
Loading history...
985
				$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
986
987
				foreach ($attachments as $attach_props) {
988
					$attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
989
					$attach_newResourceMsg = mapi_message_createattach($imessage);
990
					mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
991
					mapi_savechanges($attach_newResourceMsg);
992
				}
993
			}
994
		}
995
996
		$props = $props + $exception_props;
997
998
		// FIXME: the following piece of code is written to fix the creation
999
		// of an exception. This is only a quickfix as it is not yet possible
1000
		// to change an existing exception.
1001
		// remove mv properties when needed
1002
		foreach ($props as $propTag => $propVal) {
1003
			if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)) {
1004
				unset($props[$propTag]);
1005
			}
1006
		}
1007
1008
		mapi_setprops($imessage, $props);
1009
1010
		$this->setExceptionRecipients($imessage, $exception_recips, true);
0 ignored issues
show
$imessage of type resource is incompatible with the type resource expected by parameter $message of Recurrence::setExceptionRecipients(). ( Ignorable by Annotation )

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

1010
		$this->setExceptionRecipients(/** @scrutinizer ignore-type */ $imessage, $exception_recips, true);
Loading history...
1011
1012
		mapi_savechanges($imessage);
1013
		mapi_savechanges($attachment);
1014
	}
1015
1016
	/**
1017
	 * Function which deletes the attachment of an exception.
1018
	 *
1019
	 * @param mixed $base_date base date of the attachment. Should be in GMT. The attachment
1020
	 *                         actually saves the real time of the original date, so we have
1021
	 *                         to check whether it's on the same day.
1022
	 */
1023
	public function deleteExceptionAttachment($base_date): void {
1024
		$attachments = mapi_message_getattachmenttable($this->message);
1025
		// Retrieve only exceptions which are stored as embedded messages
1026
		$attach_res = [
1027
			RES_PROPERTY,
1028
			[
1029
				RELOP => RELOP_EQ,
1030
				ULPROPTAG => PR_ATTACH_METHOD,
1031
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
1032
			],
1033
		];
1034
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1035
1036
		foreach ($attachRows as $attachRow) {
1037
			$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1038
			$exception = mapi_attach_openobj($tempattach);
1039
1040
			$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
0 ignored issues
show
The function mapi_message_getprops was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1040
			$data = /** @scrutinizer ignore-call */ mapi_message_getprops($exception, [$this->proptags["basedate"]]);
Loading history...
1041
1042
			if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
1043
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1044
			}
1045
		}
1046
	}
1047
1048
	/**
1049
	 * Function which deletes all attachments of a message.
1050
	 */
1051
	public function deleteAttachments(): void {
1052
		$attachments = mapi_message_getattachmenttable($this->message);
1053
		$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
1054
1055
		foreach ($attachTable as $attachRow) {
1056
			if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
1057
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1058
			}
1059
		}
1060
	}
1061
1062
	/**
1063
	 * Get an exception attachment based on its basedate.
1064
	 *
1065
	 * @param mixed $base_date
1066
	 */
1067
	public function getExceptionAttachment($base_date) {
1068
		// Retrieve only exceptions which are stored as embedded messages
1069
		$attach_res = [
1070
			RES_PROPERTY,
1071
			[
1072
				RELOP => RELOP_EQ,
1073
				ULPROPTAG => PR_ATTACH_METHOD,
1074
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
1075
			],
1076
		];
1077
		$attachments = mapi_message_getattachmenttable($this->message);
1078
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1079
1080
		if (is_array($attachRows)) {
1081
			foreach ($attachRows as $attachRow) {
1082
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1083
				$exception = mapi_attach_openobj($tempattach);
1084
1085
				$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
0 ignored issues
show
The function mapi_message_getprops was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1085
				$data = /** @scrutinizer ignore-call */ mapi_message_getprops($exception, [$this->proptags["basedate"]]);
Loading history...
1086
1087
				if (!isset($data[$this->proptags["basedate"]])) {
1088
					// if no basedate found then it could be embedded message so ignore it
1089
					// we need proper restriction to exclude embedded messages as well
1090
					continue;
1091
				}
1092
1093
				if ($this->isSameDay($this->fromGMT($this->tz, $data[$this->proptags["basedate"]]), $base_date)) {
1094
					return $tempattach;
1095
				}
1096
			}
1097
		}
1098
1099
		return false;
1100
	}
1101
1102
	/**
1103
	 * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met:
1104
	 * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end>
1105
	 * - The occurrence isn't specified as a deleted occurrence.
1106
	 *
1107
	 * @param array $items        reference to the array to be added to
1108
	 * @param int   $start        start of timeframe in GMT TIME
1109
	 * @param int   $end          end of timeframe in GMT TIME
1110
	 * @param int   $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
1111
	 * @param int   $startocc     start of occurrence since beginning of day in minutes
1112
	 * @param int   $endocc       end of occurrence since beginning of day in minutes
1113
	 * @param mixed $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
1114
	 * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
1115
	 *
1116
	 * @return null|false
1117
	 */
1118
	public function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly) {
1119
		$exception = $this->isException($basedate);
1120
		if ($exception) {
1121
			return false;
1122
		}
1123
		$occstart = $basedate + $startocc * 60;
1124
		$occend = $basedate + $endocc * 60;
1125
1126
		// Convert to GMT
1127
		$occstart = $this->toGMT($tz, $occstart);
1128
		$occend = $this->toGMT($tz, $occend);
1129
1130
		/**
1131
		 * FIRST PART: Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
1132
		 * see any part of the appointment. Partial overlaps DO match.
1133
		 *
1134
		 * SECOND PART: check if occurrence is not a zero duration occurrence which
1135
		 * starts at 00:00 and ends on 00:00. if it is so, then process
1136
		 * the occurrence and send it in response.
1137
		 */
1138
		if (($occstart >= $end || $occend <= $start) && !($occstart == $occend && $occstart == $start)) {
1139
			return;
1140
		}
1141
1142
		// Properties for this occurrence are the same as the main object,
1143
		// With these properties overridden
1144
		$newitem = $this->messageprops;
1145
		$newitem[$this->proptags["startdate"]] = $occstart;
1146
		$newitem[$this->proptags["duedate"]] = $occend;
1147
		$newitem[$this->proptags["commonstart"]] = $occstart;
1148
		$newitem[$this->proptags["commonend"]] = $occend;
1149
		$newitem["basedate"] = $basedate;
1150
1151
		// If reminderonly is set, only add reminders
1152
		if ($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] == false)) {
1153
			return;
1154
		}
1155
1156
		$items[] = $newitem;
1157
	}
1158
1159
	/**
1160
	 * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe.
1161
	 *
1162
	 * @param array $items reference to the array to be added to
1163
	 * @param date  $start start of timeframe in GMT TIME
1164
	 * @param date  $end   end of timeframe in GMT TIME
1165
	 */
1166
	public function processExceptionItems(&$items, $start, $end): void {
1167
		$limit = 0;
1168
		foreach ($this->recur["changed_occurrences"] as $exception) {
1169
			// Convert to GMT
1170
			$occstart = $this->toGMT($this->tz, $exception["start"]);
1171
			$occend = $this->toGMT($this->tz, $exception["end"]);
1172
1173
			// Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
1174
			// see any part of the appointment. Partial overlaps DO match.
1175
			if ($occstart >= $end || $occend <= $start) {
1176
				continue;
1177
			}
1178
1179
			array_push($items, $this->getExceptionProperties($exception));
1180
			if (count($items) == $limit) {
1181
				break;
1182
			}
1183
		}
1184
	}
1185
1186
	/**
1187
	 * Function which verifies if on the given date an exception, delete or change, occurs.
1188
	 *
1189
	 * @param mixed $basedate
1190
	 *
1191
	 * @return bool true - if an exception occurs on the given date, false - no exception occurs on the given date
1192
	 */
1193
	public function isException($basedate) {
1194
		if ($this->isDeleteException($basedate)) {
1195
			return true;
1196
		}
1197
1198
		if ($this->getChangeException($basedate) != false) {
1199
			return true;
1200
		}
1201
1202
		return false;
1203
	}
1204
1205
	/**
1206
	 * Returns TRUE if there is a DELETE exception on the given base date.
1207
	 *
1208
	 * @param mixed $basedate
1209
	 */
1210
	public function isDeleteException($basedate): bool {
1211
		// Check if the occurrence is deleted on the specified date
1212
		foreach ($this->recur["deleted_occurrences"] as $deleted) {
1213
			if ($this->isSameDay($deleted, $basedate)) {
1214
				return true;
1215
			}
1216
		}
1217
1218
		return false;
1219
	}
1220
1221
	/**
1222
	 * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise.
1223
	 *
1224
	 * @param mixed $basedate
1225
	 */
1226
	public function getChangeException($basedate) {
1227
		// Check if the occurrence is modified on the specified date
1228
		foreach ($this->recur["changed_occurrences"] as $changed) {
1229
			if ($this->isSameDay($changed["basedate"], $basedate)) {
1230
				return $changed;
1231
			}
1232
		}
1233
1234
		return false;
1235
	}
1236
1237
	/**
1238
	 * Function to see if two dates are on the same day.
1239
	 *
1240
	 * @param mixed $date1
1241
	 * @param mixed $date2
1242
	 *
1243
	 * @return bool Returns TRUE when both dates are on the same day
1244
	 */
1245
	public function isSameDay($date1, $date2) {
1246
		$time1 = $this->gmtime($date1);
1247
		$time2 = $this->gmtime($date2);
1248
1249
		return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"];
1250
	}
1251
1252
	/**
1253
	 * Function which sets recipients for an exception.
1254
	 *
1255
	 * The $exception_recips can be provided in 2 ways:
1256
	 *  - A delta which indicates which recipients must be added, removed or deleted.
1257
	 *  - A complete array of the recipients which should be applied to the message.
1258
	 *
1259
	 * The first option is preferred as it will require less work to be executed.
1260
	 *
1261
	 * @param resource $message          exception attachment of recurring item
1262
	 * @param array    $exception_recips list of recipients
1263
	 * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1264
	 *                                   message to the attachment by default. False if only the $exception_recips changes should
1265
	 *                                   be applied.
1266
	 */
1267
	public function setExceptionRecipients($message, $exception_recips, $copy_orig_recips = true): void {
1268
		if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) {
1269
			$this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips);
1270
		}
1271
		else {
1272
			$this->setAllExceptionRecipients($message, $exception_recips);
1273
		}
1274
	}
1275
1276
	/**
1277
	 * Function which applies the provided delta for recipients changes to the exception.
1278
	 *
1279
	 * The $exception_recips should be an array containing the following keys:
1280
	 *  - "add": this contains an array of recipients which must be added
1281
	 *  - "remove": This contains an array of recipients which must be removed
1282
	 *  - "modify": This contains an array of recipients which must be modified
1283
	 *
1284
	 * @param mixed $exception
1285
	 * @param array $exception_recips list of recipients
1286
	 * @param bool  $copy_orig_recips True to copy all recipients which are on the original
1287
	 *                                message to the attachment by default. False if only the $exception_recips changes should
1288
	 *                                be applied.
1289
	 */
1290
	public function setDeltaExceptionRecipients($exception, $exception_recips, $copy_orig_recips): void {
1291
		// Check if the recipients from the original message should be copied,
1292
		// if so, open the recipient table of the parent message and apply all
1293
		// rows on the target recipient.
1294
		if ($copy_orig_recips === true) {
1295
			$origTable = mapi_message_getrecipienttable($this->message);
1296
			$recipientRows = mapi_table_queryallrows($origTable, $this->recipprops);
1297
			mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
1298
		}
1299
1300
		// Add organizer to meeting only if it is not organized.
1301
		$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']]);
1302
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1303
			$this->addOrganizer($msgprops, $exception_recips['add']);
1304
		}
1305
1306
		// Remove all deleted recipients
1307
		if (isset($exception_recips['remove'])) {
1308
			foreach ($exception_recips['remove'] as &$recip) {
1309
				if (!isset($recip[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1310
					$recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1311
				}
1312
				else {
1313
					$recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1314
				}
1315
				$recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone;		// No Response required
1316
			}
1317
			unset($recip);
1318
			mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']);
1319
		}
1320
1321
		// Add all new recipients
1322
		if (isset($exception_recips['add'])) {
1323
			mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']);
1324
		}
1325
1326
		// Modify the existing recipients
1327
		if (isset($exception_recips['modify'])) {
1328
			mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']);
1329
		}
1330
	}
1331
1332
	/**
1333
	 * Function which applies the provided recipients to the exception, also checks for deleted recipients.
1334
	 *
1335
	 * The $exception_recips should be an array containing all recipients which must be applied
1336
	 * to the exception. This will copy all recipients from the original message and then start filter
1337
	 * out all recipients which are not provided by the $exception_recips list.
1338
	 *
1339
	 * @param resource $message          exception attachment of recurring item
1340
	 * @param array    $exception_recips list of recipients
1341
	 */
1342
	public function setAllExceptionRecipients($message, $exception_recips): void {
1343
		$deletedRecipients = [];
1344
		$useMessageRecipients = false;
1345
1346
		$recipientTable = mapi_message_getrecipienttable($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_getrecipienttable(). ( Ignorable by Annotation )

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

1346
		$recipientTable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
1347
		$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1348
1349
		if (empty($recipientRows)) {
1350
			$useMessageRecipients = true;
1351
			$recipientTable = mapi_message_getrecipienttable($this->message);
1352
			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1353
		}
1354
1355
		// Add organizer to meeting only if it is not organized.
1356
		$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']]);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

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

1356
		$msgprops = mapi_getprops(/** @scrutinizer ignore-type */ $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']]);
Loading history...
1357
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1358
			$this->addOrganizer($msgprops, $exception_recips);
1359
		}
1360
1361
		if (!empty($exception_recips)) {
1362
			foreach ($recipientRows as $recipient) {
1363
				$found = false;
1364
				foreach ($exception_recips as $excep_recip) {
1365
					if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1366
						$found = true;
1367
					}
1368
				}
1369
1370
				if (!$found) {
1371
					$foundInDeletedRecipients = false;
1372
					// Look if the $recipient is in the list of deleted recipients
1373
					if (!empty($deletedRecipients)) {
1374
						foreach ($deletedRecipients as $recip) {
1375
							if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1376
								$foundInDeletedRecipients = true;
1377
								break;
1378
							}
1379
						}
1380
					}
1381
1382
					// If recipient is not in list of deleted recipient, add him
1383
					if (!$foundInDeletedRecipients) {
1384
						if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1385
							$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1386
						}
1387
						else {
1388
							$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1389
						}
1390
						$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;	// No Response required
1391
						$deletedRecipients[] = $recipient;
1392
					}
1393
				}
1394
1395
				// When $message contains a non-empty recipienttable, we must delete the recipients
1396
				// before re-adding them. However, when $message is doesn't contain any recipients,
1397
				// we are using the recipient table of the original message ($this->message)
1398
				// rather then $message. In that case, we don't need to remove the recipients
1399
				// from the $message, as the recipient table is already empty, and
1400
				// mapi_message_modifyrecipients() will throw an error.
1401
				if ($useMessageRecipients === false) {
1402
					mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_modifyrecipients(). ( Ignorable by Annotation )

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

1402
					mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_REMOVE, [$recipient]);
Loading history...
1403
				}
1404
			}
1405
			$exception_recips = array_merge($exception_recips, $deletedRecipients);
1406
		}
1407
		else {
1408
			$exception_recips = $recipientRows;
1409
		}
1410
1411
		if (!empty($exception_recips)) {
1412
			// Set the new list of recipients on the exception message, this also removes the existing recipients
1413
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $exception_recips);
1414
		}
1415
	}
1416
1417
	/**
1418
	 * Function returns basedates of all changed occurrences.
1419
	 *
1420
	 * @return array|false array( 0 => 123459321 )
1421
	 *
1422
	 * @psalm-return false|list<mixed>
1423
	 */
1424
	public function getAllExceptions() {
1425
		if (!empty($this->recur["changed_occurrences"])) {
1426
			$result = [];
1427
			foreach ($this->recur["changed_occurrences"] as $exception) {
1428
				$result[] = $exception["basedate"];
1429
			}
1430
1431
			return $result;
1432
		}
1433
1434
		return false;
1435
	}
1436
1437
	/**
1438
	 * Function which adds organizer to recipient list which is passed.
1439
	 * This function also checks if it has organizer.
1440
	 *
1441
	 * @param array $messageProps message properties
1442
	 * @param array $recipients   recipients list of message
1443
	 * @param bool  $isException  true if we are processing recipient of exception
1444
	 */
1445
	public function addOrganizer($messageProps, &$recipients, $isException = false): void {
1446
		$hasOrganizer = false;
1447
		// Check if meeting already has an organizer.
1448
		foreach ($recipients as $key => $recipient) {
1449
			if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
1450
				$hasOrganizer = true;
1451
			}
1452
			elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
1453
				// Recipients for an occurrence
1454
				$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
1455
			}
1456
		}
1457
1458
		if (!$hasOrganizer) {
1459
			// Create organizer.
1460
			$organizer = [];
1461
			$organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
1462
			$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1463
			$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1464
			$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
1465
			$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1466
			$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
1467
			$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
1468
			$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
1469
			$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
1470
1471
			// Add organizer to recipients list.
1472
			array_unshift($recipients, $organizer);
1473
		}
1474
	}
1475
}
1476
1477
/*
1478
1479
From http://www.ohelp-one.com/new-6765483-3268.html:
1480
1481
Recurrence Data Structure Offset Type Value
1482
1483
0 ULONG (?) Constant : { 0x04, 0x30, 0x04, 0x30}
1484
1485
4 UCHAR 0x0A + recurrence type: 0x0A for daily, 0x0B for weekly, 0x0C for
1486
monthly, 0x0D for yearly
1487
1488
5 UCHAR Constant: { 0x20}
1489
1490
6 ULONG Seems to be a variant of the recurrence type: 1 for daily every n
1491
days, 2 for daily every weekday and weekly, 3 for monthly or yearly. The
1492
special exception is regenerating tasks that regenerate on a weekly basis: 0
1493
is used in that case (I have no idea why).
1494
1495
Here's the recurrence-type-specific data. Because the daily every N days
1496
data are 4 bytes shorter than the data for the other types, the offsets for
1497
the rest of the data will be 4 bytes off depending on the recurrence type.
1498
1499
Daily every N days:
1500
1501
10 ULONG ( N - 1) * ( 24 * 60). I'm not sure what this is used for, but it's consistent.
1502
1503
14 ULONG N * 24 * 60: minutes between recurrences
1504
1505
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1506
regenerating tasks.
1507
1508
Daily every weekday (this is essentially a subtype of weekly recurrence):
1509
1510
10 ULONG 6 * 24 * 60: minutes between recurrences ( a week... sort of)
1511
1512
14 ULONG 1: recur every week (corresponds to the second parameter for weekly
1513
recurrence)
1514
1515
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1516
regenerating tasks.
1517
1518
22 ULONG 0x3E: bitmask for recurring every weekday (corresponds to fourth
1519
parameter for weekly recurrence)
1520
1521
Weekly every N weeks for all events and non-regenerating tasks:
1522
1523
10 ULONG 6 * 24 * 60: minutes between recurrences (a week... sort of)
1524
1525
14 ULONG N: recurrence interval
1526
1527
18 ULONG Constant: 0
1528
1529
22 ULONG Bitmask for determining which days of the week the event recurs on
1530
( 1 << dayOfWeek, where Sunday is 0).
1531
1532
Weekly every N weeks for regenerating tasks: 10 ULONG Constant: 0
1533
1534
14 ULONG N * 7 * 24 * 60: recurrence interval in minutes between occurrences
1535
1536
18 ULONG Constant: 1
1537
1538
Monthly every N months on day D:
1539
1540
10 ULONG This is the most complicated value
1541
in the entire mess. It's basically a very complicated way of stating the
1542
recurrence interval. I tweaked fbs' basic algorithm. DateTime::MonthInDays
1543
simply returns the number of days in a given month, e.g. 31 for July for 28
1544
for February (the algorithm doesn't take into account leap years, but it
1545
doesn't seem to matter). My DateTime object, like Microsoft's COleDateTime,
1546
uses 1-based months (i.e. January is 1, not 0). With that in mind, this
1547
works:
1548
1549
long monthIndex = ( ( ( ( 12 % schedule-=GetInterval()) *
1550
1551
( ( schedule-=GetStartDate().GetYear() - 1601) %
1552
1553
schedule-=GetInterval())) % schedule-=GetInterval()) +
1554
1555
( schedule-=GetStartDate().GetMonth() - 1)) % schedule-=GetInterval();
1556
1557
for( int i = 0; i < monthIndex; i++)
1558
1559
{
1560
1561
value += DateTime::GetDaysInMonth( ( i % 12) + 1) * 24 * 60;
1562
1563
}
1564
1565
This should work for any recurrence interval, including those greater than
1566
12.
1567
1568
14 ULONG N: recurrence interval
1569
1570
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1571
regenerating tasks.
1572
1573
22 ULONG D: day of month the event recurs on (if this value is greater than
1574
the number of days in a given month [e.g. 31 for and recurs in June], then
1575
the event will recur on the last day of the month)
1576
1577
Monthly every N months on the Xth Y (e.g. "2nd Tuesday"):
1578
1579
10 ULONG See above: same as for monthly every N months on day D
1580
1581
14 ULONG N: recurrence interval
1582
1583
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1584
regenerating tasks.
1585
1586
22 ULONG Y: bitmask for determining which day of the week the event recurs
1587
on (see weekly every N weeks). Some useful values are 0x7F for any day, 0x3E
1588
for a weekday, or 0x41 for a weekend day.
1589
1590
26 ULONG X: 1 for first occurrence, 2 for second, etc. 5 for last
1591
occurrence. E.g. for "2nd Tuesday", you should have values of 0x04 for the
1592
prior value and 2 for this one.
1593
1594
Yearly on day D of month M:
1595
1596
10 ULONG M (sort of): This is another messy
1597
value. It's the number of minute since the startning of the year to the
1598
given month. For an explanation of GetDaysInMonth, see monthly every N
1599
months. This will work:
1600
1601
ULONG monthOfYearInMinutes = 0;
1602
1603
for( int i = DateTime::cJanuary; i < schedule-=GetMonth(); i++)
1604
1605
{
1606
1607
monthOfYearInMinutes += DateTime::GetDaysInMonth( i) * 24 * 60;
1608
1609
}
1610
1611
1612
1613
14 ULONG 12: recurrence interval in months. Naturally, 12.
1614
1615
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1616
regenerating tasks.
1617
1618
22 ULONG D: day of month the event recurs on. See monthly every N months on
1619
day D.
1620
1621
Yearly on the Xth Y of month M: 10 ULONG M (sort of): See yearly on day D of
1622
month M.
1623
1624
14 ULONG 12: recurrence interval in months. Naturally, 12.
1625
1626
18 ULONG Constant: 0
1627
1628
22 ULONG Y: see monthly every N months on the Xth Y.
1629
1630
26 ULONG X: see monthly every N months on the Xth Y.
1631
1632
After these recurrence-type-specific values, the offsets will change
1633
depending on the type. For every type except daily every N days, the offsets
1634
will grow by at least 4. For those types using the Xth Y, the offsets will
1635
grow by an additional 4, for a total of 8. The offsets for the rest of these
1636
values will be given for the most basic case, daily every N days, i.e.
1637
without any growth. Adjust as necessary. Also, the presence of exceptions
1638
will change the offsets following the exception data by a variable number of
1639
bytes, so the offsets given in the table are accurate only for those
1640
recurrence patterns without any exceptions.
1641
1642
1643
22 UCHAR Type of pattern termination: 0x21 for terminating on a given date, 0x22 for terminating
1644
after a given number of recurrences, or 0x23 for never terminating
1645
(recurring infinitely)
1646
1647
23 UCHARx3 Constant: { 0x20, 0x00, 0x00}
1648
1649
26 ULONG Number of occurrences in pattern: 0 for infinite recurrence,
1650
otherwise supply the value, even if it terminates on a given date, not after
1651
a given number
1652
1653
30 ULONG Constant: 0
1654
1655
34 ULONG Number of exceptions to pattern (i.e. deleted or changed
1656
occurrences)
1657
1658
.... ULONGxN Base date of each exception, given in hundreds of nanoseconds
1659
since 1601, so see below to turn them into a comprehensible format. The base
1660
date of an exception is the date (and only the date-- not the time) the
1661
exception would have occurred on in the pattern. They must occur in
1662
ascending order.
1663
1664
38 ULONG Number of changed exceptions (i.e. total number of exceptions -
1665
number of deleted exceptions): if there are changed exceptions, again, more
1666
data will be needed, but that will wait
1667
1668
.... ULONGxN Start date (and only the date-- not the time) of each changed
1669
exception, i.e. the exceptions which aren't deleted. These must also occur
1670
in ascending order. If all of the exceptions are deleted, this data will be
1671
absent. If present, they will be in the format above. Any dates that are in
1672
the first list but not in the second are exceptions that have been deleted
1673
(i.e. the difference between the two sets). Note that this is the start date
1674
(including time), not the base date. Given that the values are unordered and
1675
that they can't be matched up against the previous list in this iteration of
1676
the recurrence data (they could in previous ones), it is very difficult to
1677
tell which exceptions are deleted and which are changed. Fortunately, for
1678
this new format, the base dates are given on the attachment representing the
1679
changed exception (described below), so you can simply ignore this list of
1680
changed exceptions. Just create a list of exceptions from the previous list
1681
and assume they're all deleted unless you encounter an attachment with a
1682
matching base date later on.
1683
1684
42 ULONG Start date of pattern given in hundreds of nanoseconds since 1601;
1685
see below for an explanation.
1686
1687
46 ULONG End date of pattern: see start date of pattern
1688
1689
50 ULONG Constant: { 0x06, 0x30, 0x00, 0x00}
1690
1691
NOTE: I find the following 8-byte sequence of bytes to be very useful for
1692
orienting myself when looking at the raw data. If you can find { 0x06, 0x30,
1693
0x00, 0x00, 0x08, 0x30, 0x00, 0x00}, you can use these tables to work either
1694
forwards or backwards to find the data you need. The sequence sort of
1695
delineates certain critical exception-related data and delineates the
1696
exceptions themselves from the rest of the data and is relatively easy to
1697
find. If you're going to be meddling in here a lot, I suggest making a
1698
friend of ol' 0x00003006.
1699
1700
54 UCHAR This number is some kind of version indicator. Use 0x08 for Outlook
1701
2003. I believe 0x06 is Outlook 2000 and possibly 98, while 0x07 is Outlook
1702
XP. This number must be consistent with the features of the data structure
1703
generated by the version of Outlook indicated thereby-- there are subtle
1704
differences between the structures, and, if the version doesn't match the
1705
data, Outlook will sometimes failto read the structure.
1706
1707
55 UCHARx3 Constant: { 0x30, 0x00, 0x00}
1708
1709
58 ULONG Start time of occurrence in minutes: e.g. 0 for midnight or 720 for
1710
12 PM
1711
1712
62 ULONG End time of occurrence in minutes: i.e. start time + duration, e.g.
1713
900 for an event that starts at 12 PM and ends at 3PM
1714
1715
Exception Data 66 USHORT Number of changed exceptions: essentially a check
1716
on the prior occurrence of this value; should be equivalent.
1717
1718
NOTE: The following structure will occur N many times (where N = number of
1719
changed exceptions), and each structure can be of variable length.
1720
1721
.... ULONG Start date of changed exception given in hundreds of nanoseconds
1722
since 1601
1723
1724
.... ULONG End date of changed exception given in hundreds of nanoseconds
1725
since 1601
1726
1727
.... ULONG This is a value I don't clearly understand. It seems to be some
1728
kind of archival value that matches the start time most of the time, but
1729
will lag behind when the start time is changed and then match up again under
1730
certain conditions later. In any case, setting to the same value as the
1731
start time seems to work just fine (more information on this value would be
1732
appreciated).
1733
1734
.... USHORT Bitmask of changes to the exception (see below). This will be 0
1735
if the only changes to the exception were to its start or end time.
1736
1737
.... ULONGxN Numeric values (e.g. label or minutes to remind before the
1738
event) changed in the exception. These will occur in the order of their
1739
corresponding bits (see below). If no numeric values were changed, then
1740
these values will be absent.
1741
1742
NOTE: The following three values constitute a single sub-structure that will
1743
occur N many times, where N is the number of strings that are changed in the
1744
exception. Since there are at most 2 string values that can be excepted
1745
(i.e. subject [or description], and location), there can at most be two of
1746
these, but there may be none.
1747
1748
.... USHORT Length of changed string value with NULL character
1749
1750
.... USHORT Length of changed string value without NULL character (i.e.
1751
previous value - 1)
1752
1753
.... CHARxN Changed string value (without NULL terminator)
1754
1755
Unicode Data NOTE: If a string value was changed on an exception, those
1756
changed string values will reappear here in Unicode format after 8 bytes of
1757
NULL padding (possibly a Unicode terminator?). For each exception with a
1758
changed string value, there will be an identifier, followed by the changed
1759
strings in Unicode. The strings will occur in the order of their
1760
corresponding bits (see below). E.g., if both subject and location were
1761
changed in the exception, there would be the 3-ULONG identifier, then the
1762
length of the subject, then the subject, then the length of the location,
1763
then the location.
1764
1765
70 ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}. This
1766
padding serves as a barrier between the older data structure and the
1767
appended Unicode data. This is the same sequence as the Unicode terminator,
1768
but I'm not sure whether that's its identity or not.
1769
1770
.... ULONGx3 These are the three times used to identify the exception above:
1771
start date, end date, and repeated start date. These should be the same as
1772
they were above.
1773
1774
.... USHORT Length of changed string value without NULL character. This is
1775
given as count of WCHARs, so it should be identical to the value above.
1776
1777
.... WCHARxN Changed string value in Unicode (without NULL terminator)
1778
1779
Terminator ... ULONGxN Constant: { 0x00, 0x00, 0x00, 0x00}. 4 bytes of NULL
1780
padding per changed exception. If there were no changed exceptions, all
1781
you'll need is the final terminator below.
1782
1783
.... ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}.
1784
1785
*/
1786