Passed
Push — master ( ae800f...172061 )
by
unknown
02:27
created

Recurrence::getI18nRecTermNoEnd()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 40
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 25
c 1
b 0
f 0
nc 8
nop 7
dl 0
loc 40
rs 9.52
1
<?php
2
3
/*
4
 * SPDX-License-Identifier: AGPL-3.0-only
5
 * SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH
6
 * SPDX-FileCopyrightText: Copyright 2020-2025 grommunio GmbH
7
 */
8
9
/**
10
 * Recurrence.
11
 */
12
class Recurrence extends BaseRecurrence {
13
	/*
14
	 * ABOUT TIMEZONES
15
	 *
16
	 * Timezones are rather complicated here so here are some rules to think about:
17
	 *
18
	 * - Timestamps in mapi-like properties (so in PT_SYSTIME properties) are always in GMT (including
19
	 *   the 'basedate' property in exceptions !!)
20
	 * - Timestamps for recurrence (so start/end of recurrence, and basedates for exceptions (everything
21
	 *   outside the 'basedate' property in the exception !!), and start/endtimes for exceptions) are
22
	 *   always in LOCAL time.
23
	 */
24
25
	// All properties for a recipient that are interesting
26
	public $recipprops = [
27
		PR_ENTRYID,
28
		PR_SEARCH_KEY,
29
		PR_DISPLAY_NAME,
30
		PR_EMAIL_ADDRESS,
31
		PR_RECIPIENT_ENTRYID,
32
		PR_RECIPIENT_TYPE,
33
		PR_SEND_INTERNET_ENCODING,
34
		PR_SEND_RICH_INFO,
35
		PR_RECIPIENT_DISPLAY_NAME,
36
		PR_ADDRTYPE,
37
		PR_DISPLAY_TYPE,
38
		PR_DISPLAY_TYPE_EX,
39
		PR_RECIPIENT_TRACKSTATUS,
40
		PR_RECIPIENT_TRACKSTATUS_TIME,
41
		PR_RECIPIENT_FLAGS,
42
		PR_ROWID,
43
	];
44
45
	/**
46
	 * Constructor.
47
	 *
48
	 * @param resource $store    MAPI Message Store Object
49
	 * @param mixed    $message  the MAPI (appointment) message
50
	 * @param array    $proptags an associative array of protags and their values
51
	 */
52
	public function __construct(mixed $store, mixed $message, array $proptags = []) {
53
		if (!empty($proptags)) {
54
			$this->proptags = $proptags;
55
		}
56
		else {
57
			$properties = [];
58
			$properties["entryid"] = PR_ENTRYID;
59
			$properties["parent_entryid"] = PR_PARENT_ENTRYID;
60
			$properties["message_class"] = PR_MESSAGE_CLASS;
61
			$properties["icon_index"] = PR_ICON_INDEX;
62
			$properties["subject"] = PR_SUBJECT;
63
			$properties["display_to"] = PR_DISPLAY_TO;
64
			$properties["importance"] = PR_IMPORTANCE;
65
			$properties["sensitivity"] = PR_SENSITIVITY;
66
			$properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidAppointmentStartWhole;
67
			$properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidAppointmentEndWhole;
68
			$properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:" . PidLidRecurring;
69
			$properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:" . PidLidAppointmentRecur;
70
			$properties["busystatus"] = "PT_LONG:PSETID_Appointment:" . PidLidBusyStatus;
71
			$properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
72
			$properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:" . PidLidAppointmentSubType;
73
			$properties["private"] = "PT_BOOLEAN:PSETID_Common:" . PidLidPrivate;
74
			$properties["meeting"] = "PT_LONG:PSETID_Appointment:" . PidLidAppointmentStateFlags;
75
			$properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidClipStart;
76
			$properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidClipEnd;
77
			$properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:" . PidLidRecurrencePattern;
78
			$properties["location"] = "PT_STRING8:PSETID_Appointment:" . PidLidLocation;
79
			$properties["duration"] = "PT_LONG:PSETID_Appointment:" . PidLidAppointmentDuration;
80
			$properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
81
			$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:" . PidLidReminderSet;
82
			$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:" . PidLidReminderDelta;
83
			$properties["recurrencetype"] = "PT_LONG:PSETID_Appointment:0x8231";
84
			$properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
85
			$properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
86
			$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
87
			$properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:" . PidLidReminderTime;
88
			$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
89
			$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
90
			$properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidExceptionReplaceTime;
91
			$properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:" . PidLidTimeZoneStruct;
92
			$properties["timezone"] = "PT_STRING8:PSETID_Appointment:" . PidLidTimeZoneDescription;
93
			$properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:" . PidLidReminderSignalTime;
94
			$properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
95
			$properties["hideattachments"] = "PT_BOOLEAN:PSETID_Common:" . PidLidSmartNoAttach;
96
			$properties['meetingrecurring'] = "PT_BOOLEAN:PSETID_Meeting:" . PidLidIsRecurring;
97
98
			$this->proptags = getPropIdsFromStrings($store, $properties);
99
		}
100
101
		parent::__construct($store, $message);
102
	}
103
104
	/**
105
	 * Create an exception.
106
	 *
107
	 * @param array $exception_props  the exception properties (same properties as normal recurring items)
108
	 * @param mixed $base_date        the base date of the exception (LOCAL time of non-exception occurrence)
109
	 * @param bool  $delete           true - delete occurrence, false - create new exception or modify existing
110
	 * @param array $exception_recips list of recipients
111
	 * @param mixed $copy_attach_from mapi message from which attachments should be copied
112
	 */
113
	public function createException($exception_props, $base_date, $delete = false, $exception_recips = [], $copy_attach_from = false): bool {
114
		$baseday = $this->dayStartOf($base_date);
115
		$basetime = $baseday + $this->recur["startocc"] * 60;
116
117
		// Remove any pre-existing exception on this base date
118
		if ($this->isException($baseday)) {
119
			$this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence).
120
		}
121
122
		if (!$delete) {
123
			if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
124
				return false;
125
			}
126
			$changed_item = [];
127
			// Properties in the attachment are the properties of the base object, plus $exception_props plus the base date
128
			foreach (["subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus"] as $propname) {
129
				if (isset($this->messageprops[$this->proptags[$propname]])) {
130
					$props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]];
131
					if (isset($exception_props[$this->proptags[$propname]]) &&
132
						$this->messageprops[$this->proptags[$propname]] != $exception_props[$this->proptags[$propname]]) {
133
						$changed_item[$propname] = $exception_props[$this->proptags[$propname]];
134
					}
135
				}
136
				elseif (isset($exception_props[$this->proptags[$propname]])) {
137
					$changed_item[$propname] = $exception_props[$this->proptags[$propname]];
138
				}
139
			}
140
141
			$props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
142
			unset($exception_props[PR_MESSAGE_CLASS], $exception_props[PR_ICON_INDEX]);
143
144
			$props = $exception_props + $props;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $props seems to be defined by a foreach iteration on line 128. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
145
146
			// Basedate in the exception attachment is the GMT time at which the original occurrence would have been
147
			$props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime);
148
149
			if (!isset($exception_props[$this->proptags["startdate"]])) {
150
				$props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date);
151
			}
152
153
			if (!isset($exception_props[$this->proptags["duedate"]])) {
154
				$props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date);
155
			}
156
157
			// synchronize commonstart/commonend with startdate/duedate
158
			if (isset($props[$this->proptags["startdate"]])) {
159
				$props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]];
160
			}
161
162
			if (isset($props[$this->proptags["duedate"]])) {
163
				$props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]];
164
			}
165
166
			// Save the data into an attachment
167
			$this->createExceptionAttachment($props, $exception_recips, $copy_attach_from);
0 ignored issues
show
Bug introduced by
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

167
			$this->createExceptionAttachment($props, $exception_recips, /** @scrutinizer ignore-type */ $copy_attach_from);
Loading history...
168
169
			$changed_item["basedate"] = $baseday;
170
			$changed_item["start"] = $this->fromGMT($this->tz, $props[$this->proptags["startdate"]]);
171
			$changed_item["end"] = $this->fromGMT($this->tz, $props[$this->proptags["duedate"]]);
172
173
			// Add the changed occurrence to the list
174
			$this->recur["changed_occurrences"][] = $changed_item;
175
		}
176
		else {
177
			// Delete the occurrence by placing it in the deleted occurrences list
178
			$this->recur["deleted_occurrences"][] = $baseday;
179
		}
180
181
		// Turn on hideattachments, because the attachments in this item are the exceptions
182
		mapi_setprops($this->message, [$this->proptags["hideattachments"] => true]);
183
184
		// Save recurrence data to message
185
		$this->saveRecurrence();
186
187
		return true;
188
	}
189
190
	/**
191
	 * Modifies an existing exception, but only updates the given properties
192
	 * NOTE: You can't remove properties from an exception, only add new ones.
193
	 *
194
	 * @param mixed $exception_props
195
	 * @param mixed $base_date
196
	 * @param mixed $exception_recips
197
	 * @param mixed $copy_attach_from
198
	 */
199
	public function modifyException($exception_props, $base_date, $exception_recips = [], $copy_attach_from = false): bool {
200
		if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
201
			return false;
202
		}
203
204
		$baseday = $this->dayStartOf($base_date);
205
		$extomodify = false;
206
207
		for ($i = 0, $len = count($this->recur["changed_occurrences"]); $i < $len; ++$i) {
208
			if ($this->isSameDay($this->recur["changed_occurrences"][$i]["basedate"], $baseday)) {
209
				$extomodify = &$this->recur["changed_occurrences"][$i];
210
				break;
211
			}
212
		}
213
214
		if (!$extomodify) {
215
			return false;
216
		}
217
218
		// remove basedate property as we want to preserve the old value
219
		// client will send basedate with time part as zero, so discard that value
220
		unset($exception_props[$this->proptags["basedate"]]);
221
222
		// Map of property keys to their target keys in $extomodify, with optional transformation
223
		$propertyMappings = [
224
			"startdate" => ["target" => "start", "transform" => true],
225
			"duedate" => ["target" => "end", "transform" => true],
226
			"subject" => ["target" => "subject", "transform" => false],
227
			"location" => ["target" => "location", "transform" => false],
228
			"label" => ["target" => "label", "transform" => false],
229
			"reminder" => ["target" => "reminder_set", "transform" => false],
230
			"reminder_minutes" => ["target" => "remind_before", "transform" => false],
231
			"alldayevent" => ["target" => "alldayevent", "transform" => false],
232
			"busystatus" => ["target" => "busystatus", "transform" => false],
233
		];
234
235
		foreach ($propertyMappings as $propKey => $mapping) {
236
			$propTag = $this->proptags[$propKey];
237
			if (array_key_exists($propTag, $exception_props)) {
238
				$extomodify[$mapping["target"]] = $mapping["transform"] ?
239
					$this->fromGMT($this->tz, $exception_props[$propTag]) :
240
					$exception_props[$propTag];
241
			}
242
		}
243
244
		$exception_props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
245
246
		// synchronize commonstart/commonend with startdate/duedate
247
		if (isset($exception_props[$this->proptags["startdate"]])) {
248
			$exception_props[$this->proptags["commonstart"]] = $exception_props[$this->proptags["startdate"]];
249
		}
250
251
		if (isset($exception_props[$this->proptags["duedate"]])) {
252
			$exception_props[$this->proptags["commonend"]] = $exception_props[$this->proptags["duedate"]];
253
		}
254
255
		$attach = $this->getExceptionAttachment($baseday);
256
		if (!$attach) {
257
			if ($copy_attach_from) {
258
				$this->deleteExceptionAttachment($base_date);
259
				$this->createException($exception_props, $base_date, false, $exception_recips, $copy_attach_from);
260
			}
261
			else {
262
				$this->createExceptionAttachment($exception_props, $exception_recips, $copy_attach_from);
0 ignored issues
show
Bug introduced by
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

262
				$this->createExceptionAttachment($exception_props, $exception_recips, /** @scrutinizer ignore-type */ $copy_attach_from);
Loading history...
263
			}
264
		}
265
		else {
266
			$message = mapi_attach_openobj($attach, MAPI_MODIFY);
267
268
			// Set exception properties on embedded message and save
269
			mapi_setprops($message, $exception_props);
270
			$this->setExceptionRecipients($message, $exception_recips, false);
271
			mapi_savechanges($message);
272
273
			// If a new start or duedate is provided, we update the properties 'PR_EXCEPTION_STARTTIME' and 'PR_EXCEPTION_ENDTIME'
274
			// on the attachment which holds the embedded msg and save everything.
275
			$props = [];
276
			if (isset($exception_props[$this->proptags["startdate"]])) {
277
				$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
278
			}
279
			if (isset($exception_props[$this->proptags["duedate"]])) {
280
				$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
281
			}
282
			if (!empty($props)) {
283
				mapi_setprops($attach, $props);
284
			}
285
286
			mapi_savechanges($attach);
287
		}
288
289
		// Save recurrence data to message
290
		$this->saveRecurrence();
291
292
		return true;
293
	}
294
295
	// Checks to see if the following is true:
296
	// 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)
297
	// 2) The exception to be created doesn't 'jump' over another occurrence (which may be an exception itself!)
298
	//
299
	// Both $basedate and $start are in LOCAL time
300
	public function isValidExceptionDate($basedate, $start): bool {
301
		// The way we do this is to look at the days that we're 'moving' the item in the exception. Each
302
		// of these days may only contain the item that we're modifying. Any other item violates the rules.
303
304
		if ($this->isException($basedate)) {
305
			// If we're modifying an exception, we want to look at the days that we're 'moving' compared to where
306
			// the exception used to be.
307
			$oldexception = $this->getChangeException($basedate);
308
			$prevday = $this->dayStartOf($oldexception["start"]);
309
		}
310
		else {
311
			// If its a new exception, we want to look at the original placement of this item.
312
			$prevday = $basedate;
313
		}
314
315
		$startday = $this->dayStartOf($start);
316
317
		// Get all the occurrences on the days between the basedate (may be reversed)
318
		if ($prevday < $startday) {
319
			$items = $this->getItems($this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
320
		}
321
		else {
322
			$items = $this->getItems($this->toGMT($this->tz, $startday), $this->toGMT($this->tz, $prevday + 24 * 60 * 60));
323
		}
324
325
		// There should now be exactly one item, namely the item that we are modifying. If there are any other items in the range,
326
		// then we abort the change, since one of the rules has been violated.
327
		return count($items) == 1;
328
	}
329
330
	/**
331
	 * Check to see if the exception proposed at a certain basedate is allowed concerning reminder times:.
332
	 *
333
	 * Both must be true:
334
	 * - reminder time of this item is not before the starttime of the previous recurring item
335
	 * - reminder time of the next item is not before the starttime of this item
336
	 *
337
	 * @param date   $basedate        the base date of the exception (LOCAL time of non-exception occurrence)
338
	 * @param string $reminderminutes reminder minutes which is set of the item
339
	 * @param date   $startdate       the startdate of the selected item
340
	 *
341
	 * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE)
342
	 */
343
	public function isValidReminderTime($basedate, $reminderminutes, $startdate): bool {
344
		// get all occurrence items before the selected items occurrence starttime
345
		$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate));
0 ignored issues
show
Bug introduced by
$basedate of type date is incompatible with the type integer expected by parameter $date of BaseRecurrence::toGMT(). ( Ignorable by Annotation )

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

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

911
			if (!$this->isSameDay($entry["basedate"], /** @scrutinizer ignore-type */ $base_date)) {
Loading history...
912
				$new[] = $entry;
913
			}
914
			else {
915
				$this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
916
			}
917
		}
918
919
		$this->recur["changed_occurrences"] = $new;
920
	}
921
922
	/**
923
	 * Function which saves the exception data in an attachment.
924
	 *
925
	 * @param array        $exception_props  the exception data (like any other MAPI appointment)
926
	 * @param array        $exception_recips list of recipients
927
	 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
0 ignored issues
show
Bug introduced by
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...
928
	 */
929
	public function createExceptionAttachment($exception_props, $exception_recips = [], $copy_attach_from = false): void {
930
		// Create new attachment.
931
		$attachment = mapi_message_createattach($this->message);
932
		$props = [];
933
		$props[PR_ATTACHMENT_FLAGS] = 2;
934
		$props[PR_ATTACHMENT_HIDDEN] = true;
935
		$props[PR_ATTACHMENT_LINKID] = 0;
936
		$props[PR_ATTACH_FLAGS] = 0;
937
		$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
938
		$props[PR_DISPLAY_NAME] = "Exception";
939
		$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
940
		$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
941
		mapi_setprops($attachment, $props);
942
943
		$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
944
945
		if ($copy_attach_from) {
946
			$attachmentTable = mapi_message_getattachmenttable($copy_attach_from);
947
			if ($attachmentTable) {
0 ignored issues
show
introduced by
$attachmentTable is of type resource, thus it always evaluated to true.
Loading history...
948
				$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
949
950
				foreach ($attachments as $attach_props) {
951
					$attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
952
					$attach_newResourceMsg = mapi_message_createattach($imessage);
953
					mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
954
					mapi_savechanges($attach_newResourceMsg);
955
				}
956
			}
957
		}
958
959
		$props = $props + $exception_props;
960
961
		// FIXME: the following piece of code is written to fix the creation
962
		// of an exception. This is only a quickfix as it is not yet possible
963
		// to change an existing exception.
964
		// remove mv properties when needed
965
		foreach ($props as $propTag => $propVal) {
966
			if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && $propVal === null) {
967
				unset($props[$propTag]);
968
			}
969
		}
970
971
		mapi_setprops($imessage, $props);
972
973
		$this->setExceptionRecipients($imessage, $exception_recips, true);
974
975
		mapi_savechanges($imessage);
976
		mapi_savechanges($attachment);
977
	}
978
979
	/**
980
	 * Function which deletes the attachment of an exception.
981
	 *
982
	 * @param mixed $base_date base date of the attachment. Should be in GMT. The attachment
983
	 *                         actually saves the real time of the original date, so we have
984
	 *                         to check whether it's on the same day.
985
	 */
986
	public function deleteExceptionAttachment($base_date): void {
987
		$attachments = mapi_message_getattachmenttable($this->message);
988
		// Retrieve only exceptions which are stored as embedded messages
989
		$attach_res = $this->getEmbeddedMessageRestriction();
990
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
991
992
		foreach ($attachRows as $attachRow) {
993
			$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
994
			$exception = mapi_attach_openobj($tempattach);
995
996
			$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
0 ignored issues
show
Bug introduced by
The function mapi_message_getprops was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

996
			$data = /** @scrutinizer ignore-call */ mapi_message_getprops($exception, [$this->proptags["basedate"]]);
Loading history...
997
998
			if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
999
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1000
			}
1001
		}
1002
	}
1003
1004
	/**
1005
	 * Function which deletes all attachments of a message.
1006
	 */
1007
	public function deleteAttachments(): void {
1008
		$attachments = mapi_message_getattachmenttable($this->message);
1009
		$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
1010
1011
		foreach ($attachTable as $attachRow) {
1012
			if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
1013
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1014
			}
1015
		}
1016
	}
1017
1018
	/**
1019
	 * Get an exception attachment based on its basedate.
1020
	 *
1021
	 * @param mixed $base_date
1022
	 */
1023
	public function getExceptionAttachment(int $base_date): mixed {
1024
		// Retrieve only exceptions which are stored as embedded messages
1025
		$attach_res = $this->getEmbeddedMessageRestriction();
1026
		$attachments = mapi_message_getattachmenttable($this->message);
1027
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1028
1029
		if (is_array($attachRows)) {
1030
			foreach ($attachRows as $attachRow) {
1031
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1032
				$exception = mapi_attach_openobj($tempattach);
1033
1034
				$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
0 ignored issues
show
Bug introduced by
The function mapi_message_getprops was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1034
				$data = /** @scrutinizer ignore-call */ mapi_message_getprops($exception, [$this->proptags["basedate"]]);
Loading history...
1035
1036
				if (!isset($data[$this->proptags["basedate"]])) {
1037
					// if no basedate found then it could be embedded message so ignore it
1038
					// we need proper restriction to exclude embedded messages as well
1039
					continue;
1040
				}
1041
1042
				if ($this->isSameDay($this->fromGMT($this->tz, $data[$this->proptags["basedate"]]), $base_date)) {
1043
					return $tempattach;
1044
				}
1045
			}
1046
		}
1047
1048
		return false;
1049
	}
1050
1051
	/**
1052
	 * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met:
1053
	 * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end>
1054
	 * - The occurrence isn't specified as a deleted occurrence.
1055
	 *
1056
	 * @param array $items        reference to the array to be added to
1057
	 * @param int   $start        start of timeframe in GMT TIME
1058
	 * @param int   $end          end of timeframe in GMT TIME
1059
	 * @param int   $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
1060
	 * @param int   $startocc     start of occurrence since beginning of day in minutes
1061
	 * @param int   $endocc       end of occurrence since beginning of day in minutes
1062
	 * @param mixed $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
1063
	 * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
1064
	 */
1065
	public function processOccurrenceItem(array &$items, mixed $start, int $end, mixed $basedate, mixed $startocc, mixed $endocc, mixed $tz, mixed $reminderonly): ?false {
1066
		$exception = $this->isException($basedate);
1067
		if ($exception) {
1068
			return false;
1069
		}
1070
		$occstart = $basedate + $startocc * 60;
1071
		$occend = $basedate + $endocc * 60;
1072
1073
		// Convert to GMT
1074
		$occstart = $this->toGMT($tz, $occstart);
1075
		$occend = $this->toGMT($tz, $occend);
1076
1077
		/**
1078
		 * FIRST PART: Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
1079
		 * see any part of the appointment. Partial overlaps DO match.
1080
		 *
1081
		 * SECOND PART: check if occurrence is not a zero duration occurrence which
1082
		 * starts at 00:00 and ends on 00:00. if it is so, then process
1083
		 * the occurrence and send it in response.
1084
		 */
1085
		if (($occstart >= $end || $occend <= $start) && !($occstart == $occend && $occstart == $start)) {
1086
			return null;
1087
		}
1088
1089
		// Properties for this occurrence are the same as the main object,
1090
		// With these properties overridden
1091
		$newitem = $this->messageprops;
1092
		$newitem[$this->proptags["startdate"]] = $occstart;
1093
		$newitem[$this->proptags["duedate"]] = $occend;
1094
		$newitem[$this->proptags["commonstart"]] = $occstart;
1095
		$newitem[$this->proptags["commonend"]] = $occend;
1096
		$newitem["basedate"] = $basedate;
1097
1098
		// If reminderonly is set, only add reminders
1099
		if ($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] === false)) {
1100
			return null;
1101
		}
1102
1103
		$items[] = $newitem;
1104
1105
		return null;
1106
	}
1107
1108
	/**
1109
	 * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe.
1110
	 *
1111
	 * @param array $items reference to the array to be added to
1112
	 * @param date  $start start of timeframe in GMT TIME
1113
	 * @param date  $end   end of timeframe in GMT TIME
1114
	 */
1115
	public function processExceptionItems(&$items, $start, $end): void {
1116
		$limit = 0;
1117
		foreach ($this->recur["changed_occurrences"] as $exception) {
1118
			// Convert to GMT
1119
			$occstart = $this->toGMT($this->tz, $exception["start"]);
1120
			$occend = $this->toGMT($this->tz, $exception["end"]);
1121
1122
			// Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
1123
			// see any part of the appointment. Partial overlaps DO match.
1124
			if ($occstart >= $end || $occend <= $start) {
1125
				continue;
1126
			}
1127
1128
			$items[] = $this->getExceptionProperties($exception);
1129
			if (count($items) == $limit) {
1130
				break;
1131
			}
1132
		}
1133
	}
1134
1135
	/**
1136
	 * Function which verifies if on the given date an exception, delete or change, occurs.
1137
	 *
1138
	 * @param mixed $basedate
1139
	 *
1140
	 * @return bool true - if an exception occurs on the given date, false - no exception occurs on the given date
1141
	 */
1142
	public function isException(int $basedate): bool {
1143
		if ($this->isDeleteException($basedate)) {
1144
			return true;
1145
		}
1146
1147
		if ($this->getChangeException($basedate) != false) {
0 ignored issues
show
introduced by
The condition $this->getChangeException($basedate) != false is always false.
Loading history...
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1148
			return true;
1149
		}
1150
1151
		return false;
1152
	}
1153
1154
	/**
1155
	 * Returns TRUE if there is a DELETE exception on the given base date.
1156
	 *
1157
	 * @param mixed $basedate
1158
	 */
1159
	public function isDeleteException(int $basedate): bool {
1160
		// Check if the occurrence is deleted on the specified date
1161
		foreach ($this->recur["deleted_occurrences"] as $deleted) {
1162
			if ($this->isSameDay($deleted, $basedate)) {
1163
				return true;
1164
			}
1165
		}
1166
1167
		return false;
1168
	}
1169
1170
	/**
1171
	 * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise.
1172
	 *
1173
	 * @param mixed $basedate
1174
	 */
1175
	public function getChangeException(int $basedate): array|false {
1176
		// Check if the occurrence is modified on the specified date
1177
		foreach ($this->recur["changed_occurrences"] as $changed) {
1178
			if ($this->isSameDay($changed["basedate"], $basedate)) {
1179
				return $changed;
1180
			}
1181
		}
1182
1183
		return false;
1184
	}
1185
1186
	/**
1187
	 * Function to see if two dates are on the same day.
1188
	 *
1189
	 * @param mixed $date1
1190
	 * @param mixed $date2
1191
	 *
1192
	 * @return bool Returns TRUE when both dates are on the same day
1193
	 */
1194
	public function isSameDay(int $date1, int $date2): bool {
1195
		$time1 = $this->gmtime($date1);
1196
		$time2 = $this->gmtime($date2);
1197
1198
		return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"];
1199
	}
1200
1201
	/**
1202
	 * Function which sets recipients for an exception.
1203
	 *
1204
	 * The $exception_recips can be provided in 2 ways:
1205
	 *  - A delta which indicates which recipients must be added, removed or deleted.
1206
	 *  - A complete array of the recipients which should be applied to the message.
1207
	 *
1208
	 * The first option is preferred as it will require less work to be executed.
1209
	 *
1210
	 * @param resource $message          exception attachment of recurring item
1211
	 * @param array    $exception_recips list of recipients
1212
	 * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1213
	 *                                   message to the attachment by default. False if only the $exception_recips changes should
1214
	 *                                   be applied.
1215
	 */
1216
	public function setExceptionRecipients(mixed $message, array $exception_recips, bool $copy_orig_recips = true): void {
1217
		if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) {
1218
			$this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips);
1219
		}
1220
		else {
1221
			$this->setAllExceptionRecipients($message, $exception_recips);
1222
		}
1223
	}
1224
1225
	/**
1226
	 * Function which applies the provided delta for recipients changes to the exception.
1227
	 *
1228
	 * The $exception_recips should be an array containing the following keys:
1229
	 *  - "add": this contains an array of recipients which must be added
1230
	 *  - "remove": This contains an array of recipients which must be removed
1231
	 *  - "modify": This contains an array of recipients which must be modified
1232
	 *
1233
	 * @param array $exception_recips list of recipients
1234
	 * @param bool  $copy_orig_recips True to copy all recipients which are on the original
1235
	 *                                message to the attachment by default. False if only the $exception_recips changes should
1236
	 *                                be applied.
1237
	 */
1238
	public function setDeltaExceptionRecipients(mixed $exception, array $exception_recips, bool $copy_orig_recips): void {
1239
		// Check if the recipients from the original message should be copied,
1240
		// if so, open the recipient table of the parent message and apply all
1241
		// rows on the target recipient.
1242
		if ($copy_orig_recips === true) {
1243
			$origTable = mapi_message_getrecipienttable($this->message);
1244
			$recipientRows = mapi_table_queryallrows($origTable, $this->recipprops);
1245
			mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
1246
		}
1247
1248
		// Add organizer to meeting only if it is not organized.
1249
		$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']]);
1250
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1251
			$this->addOrganizer($msgprops, $exception_recips['add']);
1252
		}
1253
1254
		// Remove all deleted recipients
1255
		if (isset($exception_recips['remove'])) {
1256
			foreach ($exception_recips['remove'] as &$recip) {
1257
				if (!isset($recip[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1258
					$recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1259
				}
1260
				else {
1261
					$recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1262
				}
1263
				$recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone;		// No Response required
1264
			}
1265
			unset($recip);
1266
			mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']);
1267
		}
1268
1269
		// Add all new recipients
1270
		if (isset($exception_recips['add'])) {
1271
			mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']);
1272
		}
1273
1274
		// Modify the existing recipients
1275
		if (isset($exception_recips['modify'])) {
1276
			mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']);
1277
		}
1278
	}
1279
1280
	/**
1281
	 * Function which applies the provided recipients to the exception, also checks for deleted recipients.
1282
	 *
1283
	 * The $exception_recips should be an array containing all recipients which must be applied
1284
	 * to the exception. This will copy all recipients from the original message and then start filter
1285
	 * out all recipients which are not provided by the $exception_recips list.
1286
	 *
1287
	 * @param resource $message          exception attachment of recurring item
1288
	 * @param array    $exception_recips list of recipients
1289
	 */
1290
	public function setAllExceptionRecipients(mixed $message, array $exception_recips): void {
1291
		$deletedRecipients = [];
1292
		$useMessageRecipients = false;
1293
1294
		$recipientTable = mapi_message_getrecipienttable($message);
1295
		$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1296
1297
		if (empty($recipientRows)) {
1298
			$useMessageRecipients = true;
1299
			$recipientTable = mapi_message_getrecipienttable($this->message);
1300
			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1301
		}
1302
1303
		// Add organizer to meeting only if it is not organized.
1304
		$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']]);
1305
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1306
			$this->addOrganizer($msgprops, $exception_recips);
1307
		}
1308
1309
		if (!empty($exception_recips)) {
1310
			foreach ($recipientRows as $recipient) {
1311
				$found = false;
1312
				foreach ($exception_recips as $excep_recip) {
1313
					if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1314
						$found = true;
1315
					}
1316
				}
1317
1318
				if (!$found) {
1319
					$foundInDeletedRecipients = false;
1320
					// Look if the $recipient is in the list of deleted recipients
1321
					if (!empty($deletedRecipients)) {
1322
						foreach ($deletedRecipients as $recip) {
1323
							if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1324
								$foundInDeletedRecipients = true;
1325
								break;
1326
							}
1327
						}
1328
					}
1329
1330
					// If recipient is not in list of deleted recipient, add him
1331
					if (!$foundInDeletedRecipients) {
1332
						if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1333
							$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1334
						}
1335
						else {
1336
							$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1337
						}
1338
						$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;	// No Response required
1339
						$deletedRecipients[] = $recipient;
1340
					}
1341
				}
1342
1343
				// When $message contains a non-empty recipienttable, we must delete the recipients
1344
				// before re-adding them. However, when $message is doesn't contain any recipients,
1345
				// we are using the recipient table of the original message ($this->message)
1346
				// rather then $message. In that case, we don't need to remove the recipients
1347
				// from the $message, as the recipient table is already empty, and
1348
				// mapi_message_modifyrecipients() will throw an error.
1349
				if ($useMessageRecipients === false) {
1350
					mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
1351
				}
1352
			}
1353
			$exception_recips = array_merge($exception_recips, $deletedRecipients);
1354
		}
1355
		else {
1356
			$exception_recips = $recipientRows;
1357
		}
1358
1359
		if (!empty($exception_recips)) {
1360
			// Set the new list of recipients on the exception message, this also removes the existing recipients
1361
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $exception_recips);
1362
		}
1363
	}
1364
1365
	/**
1366
	 * Function returns basedates of all changed occurrences.
1367
	 *
1368
	 * @return array|false array( 0 => 123459321 )
1369
	 *
1370
	 * @psalm-return false|list<mixed>
1371
	 */
1372
	public function getAllExceptions(): array|false {
1373
		if (!empty($this->recur["changed_occurrences"])) {
1374
			$result = [];
1375
			foreach ($this->recur["changed_occurrences"] as $exception) {
1376
				$result[] = $exception["basedate"];
1377
			}
1378
1379
			return $result;
1380
		}
1381
1382
		return false;
1383
	}
1384
1385
	/**
1386
	 * Function which adds organizer to recipient list which is passed.
1387
	 * This function also checks if it has organizer.
1388
	 *
1389
	 * @param array $messageProps message properties
1390
	 * @param array $recipients   recipients list of message
1391
	 * @param bool  $isException  true if we are processing recipient of exception
1392
	 */
1393
	public function addOrganizer(array $messageProps, array &$recipients, bool $isException = false): void {
1394
		$hasOrganizer = false;
1395
		// Check if meeting already has an organizer.
1396
		foreach ($recipients as $key => $recipient) {
1397
			if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
1398
				$hasOrganizer = true;
1399
			}
1400
			elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
1401
				// Recipients for an occurrence
1402
				$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
1403
			}
1404
		}
1405
1406
		if (!$hasOrganizer) {
1407
			// Create organizer.
1408
			$organizer = [];
1409
			$organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
1410
			$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1411
			$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1412
			$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
1413
			$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1414
			$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
1415
			$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
1416
			$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
1417
			$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
1418
1419
			// Add organizer to recipients list.
1420
			array_unshift($recipients, $organizer);
1421
		}
1422
	}
1423
1424
	/**
1425
	 * Returns restriction array for filtering embedded message attachments.
1426
	 *
1427
	 * @return array restriction array for ATTACH_EMBEDDED_MSG
1428
	 */
1429
	private function getEmbeddedMessageRestriction(): array {
1430
		return [
1431
			RES_PROPERTY,
1432
			[
1433
				RELOP => RELOP_EQ,
1434
				ULPROPTAG => PR_ATTACH_METHOD,
1435
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
1436
			],
1437
		];
1438
	}
1439
}
1440
1441
/*
1442
1443
From http://www.ohelp-one.com/new-6765483-3268.html:
1444
1445
Recurrence Data Structure Offset Type Value
1446
1447
0 ULONG (?) Constant : { 0x04, 0x30, 0x04, 0x30}
1448
1449
4 UCHAR 0x0A + recurrence type: 0x0A for daily, 0x0B for weekly, 0x0C for
1450
monthly, 0x0D for yearly
1451
1452
5 UCHAR Constant: { 0x20}
1453
1454
6 ULONG Seems to be a variant of the recurrence type: 1 for daily every n
1455
days, 2 for daily every weekday and weekly, 3 for monthly or yearly. The
1456
special exception is regenerating tasks that regenerate on a weekly basis: 0
1457
is used in that case (I have no idea why).
1458
1459
Here's the recurrence-type-specific data. Because the daily every N days
1460
data are 4 bytes shorter than the data for the other types, the offsets for
1461
the rest of the data will be 4 bytes off depending on the recurrence type.
1462
1463
Daily every N days:
1464
1465
10 ULONG ( N - 1) * ( 24 * 60). I'm not sure what this is used for, but it's consistent.
1466
1467
14 ULONG N * 24 * 60: minutes between recurrences
1468
1469
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1470
regenerating tasks.
1471
1472
Daily every weekday (this is essentially a subtype of weekly recurrence):
1473
1474
10 ULONG 6 * 24 * 60: minutes between recurrences ( a week... sort of)
1475
1476
14 ULONG 1: recur every week (corresponds to the second parameter for weekly
1477
recurrence)
1478
1479
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1480
regenerating tasks.
1481
1482
22 ULONG 0x3E: bitmask for recurring every weekday (corresponds to fourth
1483
parameter for weekly recurrence)
1484
1485
Weekly every N weeks for all events and non-regenerating tasks:
1486
1487
10 ULONG 6 * 24 * 60: minutes between recurrences (a week... sort of)
1488
1489
14 ULONG N: recurrence interval
1490
1491
18 ULONG Constant: 0
1492
1493
22 ULONG Bitmask for determining which days of the week the event recurs on
1494
( 1 << dayOfWeek, where Sunday is 0).
1495
1496
Weekly every N weeks for regenerating tasks: 10 ULONG Constant: 0
1497
1498
14 ULONG N * 7 * 24 * 60: recurrence interval in minutes between occurrences
1499
1500
18 ULONG Constant: 1
1501
1502
Monthly every N months on day D:
1503
1504
10 ULONG This is the most complicated value
1505
in the entire mess. It's basically a very complicated way of stating the
1506
recurrence interval. I tweaked fbs' basic algorithm. DateTime::MonthInDays
1507
simply returns the number of days in a given month, e.g. 31 for July for 28
1508
for February (the algorithm doesn't take into account leap years, but it
1509
doesn't seem to matter). My DateTime object, like Microsoft's COleDateTime,
1510
uses 1-based months (i.e. January is 1, not 0). With that in mind, this
1511
works:
1512
1513
long monthIndex = ( ( ( ( 12 % schedule-=GetInterval()) *
1514
1515
( ( schedule-=GetStartDate().GetYear() - 1601) %
1516
1517
schedule-=GetInterval())) % schedule-=GetInterval()) +
1518
1519
( schedule-=GetStartDate().GetMonth() - 1)) % schedule-=GetInterval();
1520
1521
for( int i = 0; i < monthIndex; i++)
1522
1523
{
1524
1525
value += DateTime::GetDaysInMonth( ( i % 12) + 1) * 24 * 60;
1526
1527
}
1528
1529
This should work for any recurrence interval, including those greater than
1530
12.
1531
1532
14 ULONG N: recurrence interval
1533
1534
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1535
regenerating tasks.
1536
1537
22 ULONG D: day of month the event recurs on (if this value is greater than
1538
the number of days in a given month [e.g. 31 for and recurs in June], then
1539
the event will recur on the last day of the month)
1540
1541
Monthly every N months on the Xth Y (e.g. "2nd Tuesday"):
1542
1543
10 ULONG See above: same as for monthly every N months on day D
1544
1545
14 ULONG N: recurrence interval
1546
1547
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1548
regenerating tasks.
1549
1550
22 ULONG Y: bitmask for determining which day of the week the event recurs
1551
on (see weekly every N weeks). Some useful values are 0x7F for any day, 0x3E
1552
for a weekday, or 0x41 for a weekend day.
1553
1554
26 ULONG X: 1 for first occurrence, 2 for second, etc. 5 for last
1555
occurrence. E.g. for "2nd Tuesday", you should have values of 0x04 for the
1556
prior value and 2 for this one.
1557
1558
Yearly on day D of month M:
1559
1560
10 ULONG M (sort of): This is another messy
1561
value. It's the number of minute since the startning of the year to the
1562
given month. For an explanation of GetDaysInMonth, see monthly every N
1563
months. This will work:
1564
1565
ULONG monthOfYearInMinutes = 0;
1566
1567
for( int i = DateTime::cJanuary; i < schedule-=GetMonth(); i++)
1568
1569
{
1570
1571
monthOfYearInMinutes += DateTime::GetDaysInMonth( i) * 24 * 60;
1572
1573
}
1574
1575
1576
1577
14 ULONG 12: recurrence interval in months. Naturally, 12.
1578
1579
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1580
regenerating tasks.
1581
1582
22 ULONG D: day of month the event recurs on. See monthly every N months on
1583
day D.
1584
1585
Yearly on the Xth Y of month M: 10 ULONG M (sort of): See yearly on day D of
1586
month M.
1587
1588
14 ULONG 12: recurrence interval in months. Naturally, 12.
1589
1590
18 ULONG Constant: 0
1591
1592
22 ULONG Y: see monthly every N months on the Xth Y.
1593
1594
26 ULONG X: see monthly every N months on the Xth Y.
1595
1596
After these recurrence-type-specific values, the offsets will change
1597
depending on the type. For every type except daily every N days, the offsets
1598
will grow by at least 4. For those types using the Xth Y, the offsets will
1599
grow by an additional 4, for a total of 8. The offsets for the rest of these
1600
values will be given for the most basic case, daily every N days, i.e.
1601
without any growth. Adjust as necessary. Also, the presence of exceptions
1602
will change the offsets following the exception data by a variable number of
1603
bytes, so the offsets given in the table are accurate only for those
1604
recurrence patterns without any exceptions.
1605
1606
1607
22 UCHAR Type of pattern termination: 0x21 for terminating on a given date, 0x22 for terminating
1608
after a given number of recurrences, or 0x23 for never terminating
1609
(recurring infinitely)
1610
1611
23 UCHARx3 Constant: { 0x20, 0x00, 0x00}
1612
1613
26 ULONG Number of occurrences in pattern: 0 for infinite recurrence,
1614
otherwise supply the value, even if it terminates on a given date, not after
1615
a given number
1616
1617
30 ULONG Constant: 0
1618
1619
34 ULONG Number of exceptions to pattern (i.e. deleted or changed
1620
occurrences)
1621
1622
.... ULONGxN Base date of each exception, given in hundreds of nanoseconds
1623
since 1601, so see below to turn them into a comprehensible format. The base
1624
date of an exception is the date (and only the date-- not the time) the
1625
exception would have occurred on in the pattern. They must occur in
1626
ascending order.
1627
1628
38 ULONG Number of changed exceptions (i.e. total number of exceptions -
1629
number of deleted exceptions): if there are changed exceptions, again, more
1630
data will be needed, but that will wait
1631
1632
.... ULONGxN Start date (and only the date-- not the time) of each changed
1633
exception, i.e. the exceptions which aren't deleted. These must also occur
1634
in ascending order. If all of the exceptions are deleted, this data will be
1635
absent. If present, they will be in the format above. Any dates that are in
1636
the first list but not in the second are exceptions that have been deleted
1637
(i.e. the difference between the two sets). Note that this is the start date
1638
(including time), not the base date. Given that the values are unordered and
1639
that they can't be matched up against the previous list in this iteration of
1640
the recurrence data (they could in previous ones), it is very difficult to
1641
tell which exceptions are deleted and which are changed. Fortunately, for
1642
this new format, the base dates are given on the attachment representing the
1643
changed exception (described below), so you can simply ignore this list of
1644
changed exceptions. Just create a list of exceptions from the previous list
1645
and assume they're all deleted unless you encounter an attachment with a
1646
matching base date later on.
1647
1648
42 ULONG Start date of pattern given in hundreds of nanoseconds since 1601;
1649
see below for an explanation.
1650
1651
46 ULONG End date of pattern: see start date of pattern
1652
1653
50 ULONG Constant: { 0x06, 0x30, 0x00, 0x00}
1654
1655
NOTE: I find the following 8-byte sequence of bytes to be very useful for
1656
orienting myself when looking at the raw data. If you can find { 0x06, 0x30,
1657
0x00, 0x00, 0x08, 0x30, 0x00, 0x00}, you can use these tables to work either
1658
forwards or backwards to find the data you need. The sequence sort of
1659
delineates certain critical exception-related data and delineates the
1660
exceptions themselves from the rest of the data and is relatively easy to
1661
find. If you're going to be meddling in here a lot, I suggest making a
1662
friend of ol' 0x00003006.
1663
1664
54 UCHAR This number is some kind of version indicator. Use 0x08 for Outlook
1665
2003. I believe 0x06 is Outlook 2000 and possibly 98, while 0x07 is Outlook
1666
XP. This number must be consistent with the features of the data structure
1667
generated by the version of Outlook indicated thereby-- there are subtle
1668
differences between the structures, and, if the version doesn't match the
1669
data, Outlook will sometimes failto read the structure.
1670
1671
55 UCHARx3 Constant: { 0x30, 0x00, 0x00}
1672
1673
58 ULONG Start time of occurrence in minutes: e.g. 0 for midnight or 720 for
1674
12 PM
1675
1676
62 ULONG End time of occurrence in minutes: i.e. start time + duration, e.g.
1677
900 for an event that starts at 12 PM and ends at 3PM
1678
1679
Exception Data 66 USHORT Number of changed exceptions: essentially a check
1680
on the prior occurrence of this value; should be equivalent.
1681
1682
NOTE: The following structure will occur N many times (where N = number of
1683
changed exceptions), and each structure can be of variable length.
1684
1685
.... ULONG Start date of changed exception given in hundreds of nanoseconds
1686
since 1601
1687
1688
.... ULONG End date of changed exception given in hundreds of nanoseconds
1689
since 1601
1690
1691
.... ULONG This is a value I don't clearly understand. It seems to be some
1692
kind of archival value that matches the start time most of the time, but
1693
will lag behind when the start time is changed and then match up again under
1694
certain conditions later. In any case, setting to the same value as the
1695
start time seems to work just fine (more information on this value would be
1696
appreciated).
1697
1698
.... USHORT Bitmask of changes to the exception (see below). This will be 0
1699
if the only changes to the exception were to its start or end time.
1700
1701
.... ULONGxN Numeric values (e.g. label or minutes to remind before the
1702
event) changed in the exception. These will occur in the order of their
1703
corresponding bits (see below). If no numeric values were changed, then
1704
these values will be absent.
1705
1706
NOTE: The following three values constitute a single sub-structure that will
1707
occur N many times, where N is the number of strings that are changed in the
1708
exception. Since there are at most 2 string values that can be excepted
1709
(i.e. subject [or description], and location), there can at most be two of
1710
these, but there may be none.
1711
1712
.... USHORT Length of changed string value with NULL character
1713
1714
.... USHORT Length of changed string value without NULL character (i.e.
1715
previous value - 1)
1716
1717
.... CHARxN Changed string value (without NULL terminator)
1718
1719
Unicode Data NOTE: If a string value was changed on an exception, those
1720
changed string values will reappear here in Unicode format after 8 bytes of
1721
NULL padding (possibly a Unicode terminator?). For each exception with a
1722
changed string value, there will be an identifier, followed by the changed
1723
strings in Unicode. The strings will occur in the order of their
1724
corresponding bits (see below). E.g., if both subject and location were
1725
changed in the exception, there would be the 3-ULONG identifier, then the
1726
length of the subject, then the subject, then the length of the location,
1727
then the location.
1728
1729
70 ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}. This
1730
padding serves as a barrier between the older data structure and the
1731
appended Unicode data. This is the same sequence as the Unicode terminator,
1732
but I'm not sure whether that's its identity or not.
1733
1734
.... ULONGx3 These are the three times used to identify the exception above:
1735
start date, end date, and repeated start date. These should be the same as
1736
they were above.
1737
1738
.... USHORT Length of changed string value without NULL character. This is
1739
given as count of WCHARs, so it should be identical to the value above.
1740
1741
.... WCHARxN Changed string value in Unicode (without NULL terminator)
1742
1743
Terminator ... ULONGxN Constant: { 0x00, 0x00, 0x00, 0x00}. 4 bytes of NULL
1744
padding per changed exception. If there were no changed exceptions, all
1745
you'll need is the final terminator below.
1746
1747
.... ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}.
1748
1749
*/
1750