Passed
Push — master ( 172061...3e865e )
by
unknown
02:11
created

Recurrence   F

Complexity

Total Complexity 177

Size/Duplication

Total Lines 1384
Duplicated Lines 0 %

Importance

Changes 16
Bugs 1 Features 4
Metric Value
eloc 626
c 16
b 1
f 4
dl 0
loc 1384
rs 1.974
wmc 177

37 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 50 2
A isValidReminderTime() 0 31 5
C createException() 0 75 14
A isValidExceptionDate() 0 28 3
F modifyException() 0 94 16
A getOccurrenceStart() 0 4 1
A setRecurrence() 0 23 5
A getOccurrenceEnd() 0 4 1
A getI18nRecTermNrOcc() 0 65 4
A getExceptionAttachment() 0 26 5
A getI18RecTypeDaily() 0 23 3
A isException() 0 10 3
A getCalendarItems() 0 2 1
A isSameDay() 0 5 3
A getChangeException() 0 9 3
A getI18nRecTermDate() 0 45 4
B setDeltaExceptionRecipients() 0 39 10
B saveRecurrencePattern() 0 77 6
A getOccDate() 0 8 4
A deleteExceptionAttachment() 0 14 3
A getI18nRecTermNoEnd() 0 40 4
A setExceptionRecipients() 0 6 4
A getAllExceptions() 0 11 3
A isDeleteException() 0 9 3
A getI18RecTypeYearly() 0 16 2
A getI18RecTypeWeekly() 0 26 5
A deleteAttachments() 0 7 4
A getI18nTime() 0 2 1
A getEmbeddedMessageRestriction() 0 7 1
D setAllExceptionRecipients() 0 72 19
B createExceptionAttachment() 0 48 7
A getI18nRecurrenceType() 0 18 1
B processOccurrenceItem() 0 41 9
A getNextReminderTime() 0 29 3
A deleteException() 0 25 5
B addOrganizer() 0 28 8
A getI18RecTypeMonthly() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like Recurrence often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Recurrence, and based on these observations, apply Extract Interface, too.

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

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

994
			$data = /** @scrutinizer ignore-call */ mapi_message_getprops($exception, [$this->proptags["basedate"]]);
Loading history...
995
996
			if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
997
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
998
			}
999
		}
1000
	}
1001
1002
	/**
1003
	 * Function which deletes all attachments of a message.
1004
	 */
1005
	public function deleteAttachments(): void {
1006
		$attachments = mapi_message_getattachmenttable($this->message);
1007
		$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
1008
1009
		foreach ($attachTable as $attachRow) {
1010
			if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
1011
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1012
			}
1013
		}
1014
	}
1015
1016
	/**
1017
	 * Get an exception attachment based on its basedate.
1018
	 */
1019
	public function getExceptionAttachment(int $base_date): mixed {
1020
		// Retrieve only exceptions which are stored as embedded messages
1021
		$attach_res = $this->getEmbeddedMessageRestriction();
1022
		$attachments = mapi_message_getattachmenttable($this->message);
1023
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1024
1025
		if (is_array($attachRows)) {
1026
			foreach ($attachRows as $attachRow) {
1027
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1028
				$exception = mapi_attach_openobj($tempattach);
1029
1030
				$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

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