Passed
Push — master ( 8aba83...48d49b )
by
unknown
18:08 queued 08:09
created

Recurrence::setExceptionRecipients()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 4
nc 2
nop 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
/*
3
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
6
 */
7
8
	/**
9
	 * Recurrence.
10
	 */
11
	class Recurrence extends BaseRecurrence {
12
		/*
13
		 * ABOUT TIMEZONES
14
		 *
15
		 * Timezones are rather complicated here so here are some rules to think about:
16
		 *
17
		 * - Timestamps in mapi-like properties (so in PT_SYSTIME properties) are always in GMT (including
18
		 *   the 'basedate' property in exceptions !!)
19
		 * - Timestamps for recurrence (so start/end of recurrence, and basedates for exceptions (everything
20
		 *   outside the 'basedate' property in the exception !!), and start/endtimes for exceptions) are
21
		 *   always in LOCAL time.
22
		 */
23
24
		// All properties for a recipient that are interesting
25
		public $recipprops = [
26
			PR_ENTRYID,
27
			PR_SEARCH_KEY,
28
			PR_DISPLAY_NAME,
29
			PR_EMAIL_ADDRESS,
30
			PR_RECIPIENT_ENTRYID,
31
			PR_RECIPIENT_TYPE,
32
			PR_SEND_INTERNET_ENCODING,
33
			PR_SEND_RICH_INFO,
34
			PR_RECIPIENT_DISPLAY_NAME,
35
			PR_ADDRTYPE,
36
			PR_DISPLAY_TYPE,
37
			PR_DISPLAY_TYPE_EX,
38
			PR_RECIPIENT_TRACKSTATUS,
39
			PR_RECIPIENT_TRACKSTATUS_TIME,
40
			PR_RECIPIENT_FLAGS,
41
			PR_ROWID,
42
		];
43
44
		/**
45
		 * Constructor.
46
		 *
47
		 * @param resource $store    MAPI Message Store Object
48
		 * @param resource $message  the MAPI (appointment) message
49
		 * @param array    $proptags an associative array of protags and their values
50
		 */
51
		public function __construct($store, $message, $proptags = []) {
52
			if ($proptags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $proptags of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
53
				$this->proptags = $proptags;
54
			}
55
			else {
56
				$properties = [];
57
				$properties["entryid"] = PR_ENTRYID;
58
				$properties["parent_entryid"] = PR_PARENT_ENTRYID;
59
				$properties["message_class"] = PR_MESSAGE_CLASS;
60
				$properties["icon_index"] = PR_ICON_INDEX;
61
				$properties["subject"] = PR_SUBJECT;
62
				$properties["display_to"] = PR_DISPLAY_TO;
63
				$properties["importance"] = PR_IMPORTANCE;
64
				$properties["sensitivity"] = PR_SENSITIVITY;
65
				$properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:".PidLidAppointmentStartWhole;
66
				$properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:".PidLidAppointmentEndWhole;
67
				$properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:".PidLidRecurring;
68
				$properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:".PidLidAppointmentRecur;
69
				$properties["busystatus"] = "PT_LONG:PSETID_Appointment:".PidLidBusyStatus;
70
				$properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
71
				$properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:".PidLidAppointmentSubType;
72
				$properties["private"] = "PT_BOOLEAN:PSETID_Common:".PidLidPrivate;
73
				$properties["meeting"] = "PT_LONG:PSETID_Appointment:".PidLidAppointmentStateFlags;
74
				$properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:".PidLidClipStart;
75
				$properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:".PidLidClipEnd;
76
				$properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment:0x8232";
77
				$properties["location"] = "PT_STRING8:PSETID_Appointment:".PidLidLocation;
78
				$properties["duration"] = "PT_LONG:PSETID_Appointment:".PidLidAppointmentDuration;
79
				$properties["responsestatus"] = "PT_LONG:PSETID_Appointment:0x8218";
80
				$properties["reminder"] = "PT_BOOLEAN:PSETID_Common:".PidLidReminderSet;
81
				$properties["reminder_minutes"] = "PT_LONG:PSETID_Common:".PidLidReminderDelta;
82
				$properties["recurrencetype"] = "PT_LONG:PSETID_Appointment:0x8231";
83
				$properties["contacts"] = "PT_MV_STRING8:PSETID_Common:0x853a";
84
				$properties["contacts_string"] = "PT_STRING8:PSETID_Common:0x8586";
85
				$properties["categories"] = "PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords";
86
				$properties["reminder_time"] = "PT_SYSTIME:PSETID_Common:".PidLidReminderTime;
87
				$properties["commonstart"] = "PT_SYSTIME:PSETID_Common:0x8516";
88
				$properties["commonend"] = "PT_SYSTIME:PSETID_Common:0x8517";
89
				$properties["basedate"] = "PT_SYSTIME:PSETID_Appointment:".PidLidExceptionReplaceTime;
90
				$properties["timezone_data"] = "PT_BINARY:PSETID_Appointment:".PidLidTimeZoneStruct;
91
				$properties["timezone"] = "PT_STRING8:PSETID_Appointment:".PidLidTimeZoneDescription;
92
				$properties["flagdueby"] = "PT_SYSTIME:PSETID_Common:".PidLidReminderSignalTime;
93
				$properties["side_effects"] = "PT_LONG:PSETID_Common:0x8510";
94
				$properties["hideattachments"] = "PT_BOOLEAN:PSETID_Common:".PidLidSmartNoAttach;
95
96
				$this->proptags = getPropIdsFromStrings($store, $properties);
97
			}
98
99
			parent::__construct($store, $message);
100
		}
101
102
		/**
103
		 * Create an exception.
104
		 *
105
		 * @param array        $exception_props  the exception properties (same properties as normal recurring items)
106
		 * @param date         $base_date        the base date of the exception (LOCAL time of non-exception occurrence)
107
		 * @param bool         $delete           true - delete occurrence, false - create new exception or modify existing
108
		 * @param array        $exception_recips true - delete occurrence, false - create new exception or modify existing
109
		 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
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...
110
		 */
111
		public function createException($exception_props, $base_date, $delete = false, $exception_recips = [], $copy_attach_from = false) {
112
			$baseday = $this->dayStartOf($base_date);
113
			$basetime = $baseday + $this->recur["startocc"] * 60;
114
115
			// Remove any pre-existing exception on this base date
116
			if ($this->isException($baseday)) {
117
				$this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence).
118
			}
119
120
			if (!$delete) {
121
				if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
122
					return false;
123
				}
124
				// Properties in the attachment are the properties of the base object, plus $exception_props plus the base date
125
				foreach (["subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus"] as $propname) {
126
					if (isset($this->messageprops[$this->proptags[$propname]])) {
127
						$props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]];
128
					}
129
				}
130
131
				$props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
132
				unset($exception_props[PR_MESSAGE_CLASS], $exception_props[PR_ICON_INDEX]);
133
134
				$props = $exception_props + $props;
135
136
				// Basedate in the exception attachment is the GMT time at which the original occurrence would have been
137
				$props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime);
138
139
				if (!isset($exception_props[$this->proptags["startdate"]])) {
140
					$props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date);
141
				}
142
143
				if (!isset($exception_props[$this->proptags["duedate"]])) {
144
					$props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date);
145
				}
146
147
				// synchronize commonstart/commonend with startdate/duedate
148
				if (isset($props[$this->proptags["startdate"]])) {
149
					$props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]];
150
				}
151
152
				if (isset($props[$this->proptags["duedate"]])) {
153
					$props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]];
154
				}
155
156
				// Save the data into an attachment
157
				$this->createExceptionAttachment($props, $exception_recips, $copy_attach_from);
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

157
				$this->createExceptionAttachment($props, $exception_recips, /** @scrutinizer ignore-type */ $copy_attach_from);
Loading history...
158
159
				$changed_item = [];
160
161
				$changed_item["basedate"] = $baseday;
162
				$changed_item["start"] = $this->fromGMT($this->tz, $props[$this->proptags["startdate"]]);
163
				$changed_item["end"] = $this->fromGMT($this->tz, $props[$this->proptags["duedate"]]);
164
165
				if (array_key_exists($this->proptags["subject"], $exception_props)) {
166
					$changed_item["subject"] = $exception_props[$this->proptags["subject"]];
167
				}
168
169
				if (array_key_exists($this->proptags["location"], $exception_props)) {
170
					$changed_item["location"] = $exception_props[$this->proptags["location"]];
171
				}
172
173
				if (array_key_exists($this->proptags["label"], $exception_props)) {
174
					$changed_item["label"] = $exception_props[$this->proptags["label"]];
175
				}
176
177
				if (array_key_exists($this->proptags["reminder"], $exception_props)) {
178
					$changed_item["reminder_set"] = $exception_props[$this->proptags["reminder"]];
179
				}
180
181
				if (array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
182
					$changed_item["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
183
				}
184
185
				if (array_key_exists($this->proptags["alldayevent"], $exception_props)) {
186
					$changed_item["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
187
				}
188
189
				if (array_key_exists($this->proptags["busystatus"], $exception_props)) {
190
					$changed_item["busystatus"] = $exception_props[$this->proptags["busystatus"]];
191
				}
192
193
				// Add the changed occurrence to the list
194
				array_push($this->recur["changed_occurrences"], $changed_item);
195
			}
196
			else {
197
				// Delete the occurrence by placing it in the deleted occurrences list
198
				array_push($this->recur["deleted_occurrences"], $baseday);
199
			}
200
201
			// Turn on hideattachments, because the attachments in this item are the exceptions
202
			mapi_setprops($this->message, [$this->proptags["hideattachments"] => true]);
203
204
			// Save recurrence data to message
205
			$this->saveRecurrence();
206
207
			return true;
208
		}
209
210
		/**
211
		 * Modifies an existing exception, but only updates the given properties
212
		 * NOTE: You can't remove properties from an exception, only add new ones.
213
		 *
214
		 * @param mixed $exception_props
215
		 * @param mixed $base_date
216
		 * @param mixed $exception_recips
217
		 * @param mixed $copy_attach_from
218
		 */
219
		public function modifyException($exception_props, $base_date, $exception_recips = [], $copy_attach_from = false) {
220
			if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
221
				return false;
222
			}
223
224
			$baseday = $this->dayStartOf($base_date);
225
			$extomodify = false;
226
227
			for ($i = 0, $len = count($this->recur["changed_occurrences"]); $i < $len; ++$i) {
228
				if ($this->isSameDay($this->recur["changed_occurrences"][$i]["basedate"], $baseday)) {
229
					$extomodify = &$this->recur["changed_occurrences"][$i];
230
				}
231
			}
232
233
			if (!$extomodify) {
0 ignored issues
show
introduced by
$extomodify is of type mixed, thus it always evaluated to false.
Loading history...
234
				return false;
235
			}
236
237
			// remove basedate property as we want to preserve the old value
238
			// client will send basedate with time part as zero, so discard that value
239
			unset($exception_props[$this->proptags["basedate"]]);
240
241
			if (array_key_exists($this->proptags["startdate"], $exception_props)) {
242
				$extomodify["start"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
243
			}
244
245
			if (array_key_exists($this->proptags["duedate"], $exception_props)) {
246
				$extomodify["end"] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
247
			}
248
249
			if (array_key_exists($this->proptags["subject"], $exception_props)) {
250
				$extomodify["subject"] = $exception_props[$this->proptags["subject"]];
251
			}
252
253
			if (array_key_exists($this->proptags["location"], $exception_props)) {
254
				$extomodify["location"] = $exception_props[$this->proptags["location"]];
255
			}
256
257
			if (array_key_exists($this->proptags["label"], $exception_props)) {
258
				$extomodify["label"] = $exception_props[$this->proptags["label"]];
259
			}
260
261
			if (array_key_exists($this->proptags["reminder"], $exception_props)) {
262
				$extomodify["reminder_set"] = $exception_props[$this->proptags["reminder"]];
263
			}
264
265
			if (array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
266
				$extomodify["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
267
			}
268
269
			if (array_key_exists($this->proptags["alldayevent"], $exception_props)) {
270
				$extomodify["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
271
			}
272
273
			if (array_key_exists($this->proptags["busystatus"], $exception_props)) {
274
				$extomodify["busystatus"] = $exception_props[$this->proptags["busystatus"]];
275
			}
276
277
			$exception_props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
278
279
			// synchronize commonstart/commonend with startdate/duedate
280
			if (isset($exception_props[$this->proptags["startdate"]])) {
281
				$exception_props[$this->proptags["commonstart"]] = $exception_props[$this->proptags["startdate"]];
282
			}
283
284
			if (isset($exception_props[$this->proptags["duedate"]])) {
285
				$exception_props[$this->proptags["commonend"]] = $exception_props[$this->proptags["duedate"]];
286
			}
287
288
			$attach = $this->getExceptionAttachment($baseday);
289
			if (!$attach) {
290
				if ($copy_attach_from) {
291
					$this->deleteExceptionAttachment($base_date);
292
					$this->createException($exception_props, $base_date, false, $exception_recips, $copy_attach_from);
293
				}
294
				else {
295
					$this->createExceptionAttachment($exception_props, $exception_recips, $copy_attach_from);
296
				}
297
			}
298
			else {
299
				$message = mapi_attach_openobj($attach, MAPI_MODIFY);
300
301
				// Set exception properties on embedded message and save
302
				mapi_setprops($message, $exception_props);
303
				$this->setExceptionRecipients($message, $exception_recips, false);
304
				mapi_savechanges($message);
305
306
				// If a new start or duedate is provided, we update the properties 'PR_EXCEPTION_STARTTIME' and 'PR_EXCEPTION_ENDTIME'
307
				// on the attachment which holds the embedded msg and save everything.
308
				$props = [];
309
				if (isset($exception_props[$this->proptags["startdate"]])) {
310
					$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
311
				}
312
				if (isset($exception_props[$this->proptags["duedate"]])) {
313
					$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
314
				}
315
				if (!empty($props)) {
316
					mapi_setprops($attach, $props);
317
				}
318
319
				mapi_savechanges($attach);
320
			}
321
322
			// Save recurrence data to message
323
			$this->saveRecurrence();
324
325
			return true;
326
		}
327
328
		// Checks to see if the following is true:
329
		// 1) The exception to be created doesn't create two exceptions starting on one day (however, they can END on the same day by modifying duration)
330
		// 2) The exception to be created doesn't 'jump' over another occurrence (which may be an exception itself!)
331
		//
332
		// Both $basedate and $start are in LOCAL time
333
		public function isValidExceptionDate($basedate, $start) {
334
			// The way we do this is to look at the days that we're 'moving' the item in the exception. Each
335
			// of these days may only contain the item that we're modifying. Any other item violates the rules.
336
337
			if ($this->isException($basedate)) {
338
				// If we're modifying an exception, we want to look at the days that we're 'moving' compared to where
339
				// the exception used to be.
340
				$oldexception = $this->getChangeException($basedate);
341
				$prevday = $this->dayStartOf($oldexception["start"]);
342
			}
343
			else {
344
				// If its a new exception, we want to look at the original placement of this item.
345
				$prevday = $basedate;
346
			}
347
348
			$startday = $this->dayStartOf($start);
349
350
			// Get all the occurrences on the days between the basedate (may be reversed)
351
			if ($prevday < $startday) {
352
				$items = $this->getItems($this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
0 ignored issues
show
Bug introduced by
$this->toGMT($this->tz, $startday + 24 * 60 * 60) of type integer is incompatible with the type date expected by parameter $end of BaseRecurrence::getItems(). ( Ignorable by Annotation )

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

352
				$items = $this->getItems($this->toGMT($this->tz, $prevday), /** @scrutinizer ignore-type */ $this->toGMT($this->tz, $startday + 24 * 60 * 60));
Loading history...
Bug introduced by
It seems like $this->toGMT($this->tz, $prevday) can also be of type integer; however, parameter $start of BaseRecurrence::getItems() does only seem to accept date, 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

352
				$items = $this->getItems(/** @scrutinizer ignore-type */ $this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
Loading history...
353
			}
354
			else {
355
				$items = $this->getItems($this->toGMT($this->tz, $startday), $this->toGMT($this->tz, $prevday + 24 * 60 * 60));
356
			}
357
358
			// There should now be exactly one item, namely the item that we are modifying. If there are any other items in the range,
359
			// then we abort the change, since one of the rules has been violated.
360
			return count($items) == 1;
361
		}
362
363
		/**
364
		 * Check to see if the exception proposed at a certain basedate is allowed concerning reminder times:.
365
		 *
366
		 * Both must be true:
367
		 * - reminder time of this item is not before the starttime of the previous recurring item
368
		 * - reminder time of the next item is not before the starttime of this item
369
		 *
370
		 * @param date   $basedate        the base date of the exception (LOCAL time of non-exception occurrence)
371
		 * @param string $reminderminutes reminder minutes which is set of the item
372
		 * @param date   $startdate       the startdate of the selected item
373
		 * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE)
374
		 */
375
		public function isValidReminderTime($basedate, $reminderminutes, $startdate) {
376
			// get all occurrence items before the seleceted items occurrence starttime
377
			$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate));
0 ignored issues
show
Bug introduced by
It seems like $this->toGMT($this->tz, $basedate) can also be of type integer; however, parameter $end of BaseRecurrence::getItems() does only seem to accept date, 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

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

390
			$currentOcc = $this->getItems(/** @scrutinizer ignore-type */ $this->toGMT($this->tz, $basedate), 0x7FF00000, 2, true);
Loading history...
391
392
			// If there are another two occurrences, then the first is the current occurrence, and the one after that
393
			// is the next occurrence.
394
			if (count($currentOcc) > 1) {
395
				$next = $currentOcc[1];
396
				// Get reminder time of the next occurrence.
397
				$nextOccReminderTime = $next[$this->proptags["startdate"]] - ($next[$this->proptags["reminder_minutes"]] * 60);
398
				// If the reminder time of the next item is before the start of this item, then that's not allowed
399
				if ($nextOccReminderTime <= $startdate) {
400
					return false;
401
				}
402
			}
403
404
			// All was ok
405
			return true;
406
		}
407
408
		public function setRecurrence($tz, $recur) {
409
			// only reset timezone if specified
410
			if ($tz) {
411
				$this->tz = $tz;
412
			}
413
414
			$this->recur = $recur;
415
416
			if (!isset($this->recur["changed_occurrences"])) {
417
				$this->recur["changed_occurrences"] = [];
418
			}
419
420
			if (!isset($this->recur["deleted_occurrences"])) {
421
				$this->recur["deleted_occurrences"] = [];
422
			}
423
424
			$this->deleteAttachments();
425
			$this->saveRecurrence();
426
427
			// if client has not set the recurring_pattern then we should generate it and save it
428
			$messageProps = mapi_getprops($this->message, [$this->proptags["recurring_pattern"]]);
429
			if (empty($messageProps[$this->proptags["recurring_pattern"]])) {
430
				$this->saveRecurrencePattern();
431
			}
432
		}
433
434
		// Returns the start or end time of the occurrence on the given base date.
435
		// This assumes that the basedate you supply is in LOCAL time
436
		public function getOccurrenceStart($basedate) {
437
			$daystart = $this->dayStartOf($basedate);
438
439
			return $this->toGMT($this->tz, $daystart + $this->recur["startocc"] * 60);
440
		}
441
442
		public function getOccurrenceEnd($basedate) {
443
			$daystart = $this->dayStartOf($basedate);
444
445
			return $this->toGMT($this->tz, $daystart + $this->recur["endocc"] * 60);
446
		}
447
448
		/**
449
		 * This function returns the next remindertime starting from $timestamp
450
		 * When no next reminder exists, false is returned.
451
		 *
452
		 * Note: Before saving this new reminder time (when snoozing), you must check for
453
		 *       yourself if this reminder time is earlier than your snooze time, else
454
		 *       use your snooze time and not this reminder time.
455
		 *
456
		 * @param mixed $timestamp
457
		 */
458
		public function getNextReminderTime($timestamp) {
459
			/**
460
			 * Get next item from now until forever, but max 1 item with reminder set
461
			 * Note 0x7ff00000 instead of 0x7fffffff because of possible overflow failures when converting to GMT....
462
			 * Here for getting next 10 occurrences assuming that next here we will be able to find
463
			 * nextreminder occurrence in 10 occurrences.
464
			 */
465
			$items = $this->getItems($timestamp, 0x7FF00000, 10, true);
0 ignored issues
show
Bug introduced by
2146435072 of type integer is incompatible with the type date expected by parameter $end of BaseRecurrence::getItems(). ( Ignorable by Annotation )

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

465
			$items = $this->getItems($timestamp, /** @scrutinizer ignore-type */ 0x7FF00000, 10, true);
Loading history...
466
467
			// Initially setting nextreminder to false so when no next reminder exists, false is returned.
468
			$nextreminder = false;
469
			/*
470
			 * Loop through all reminder which we get in items variable
471
			 * and check whether the remindertime is greater than timestamp.
472
			 * On the first occurrence of greater nextreminder break the loop
473
			 * and return the value to calling function.
474
			 */
475
			for ($i = 0, $len = count($items); $i < $len; ++$i) {
476
				$item = $items[$i];
477
				$tempnextreminder = $item[$this->proptags["startdate"]] - ($item[$this->proptags["reminder_minutes"]] * 60);
478
479
				// If tempnextreminder is greater than timestamp then save it in nextreminder and break from the loop.
480
				if ($tempnextreminder > $timestamp) {
481
					$nextreminder = $tempnextreminder;
482
					break;
483
				}
484
			}
485
486
			return $nextreminder;
487
		}
488
489
		/**
490
		 * Note: Static function, more like a utility function.
491
		 *
492
		 * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are
493
		 * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval
494
		 * 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
495
		 * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>.
496
		 *
497
		 * @param $store resource The store in which the calendar resides
498
		 * @param $calendar resource The calendar to get the items from
499
		 * @param $viewstart int Timestamp of beginning of view window
500
		 * @param $viewend int Timestamp of end of view window
501
		 * @param $propsrequested array Array of properties to return
502
		 * @param $rows array Array of rowdata as if they were returned directly from mapi_table_queryrows. Each recurring item is
503
		 *                    expanded so that it seems that there are only many single appointments in the table.
504
		 */
505
		public static function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested) {
506
			return getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested);
507
		}
508
509
		/*
510
		 * CODE BELOW THIS LINE IS FOR INTERNAL USE ONLY
511
		 *****************************************************************************************************************
512
		 */
513
514
		/**
515
		 * Generates and stores recurrence pattern string to recurring_pattern property.
516
		 */
517
		public function saveRecurrencePattern() {
518
			// Start formatting the properties in such a way we can apply
519
			// them directly into the recurrence pattern.
520
			$type = $this->recur['type'];
521
			$everyn = $this->recur['everyn'];
522
			$start = $this->recur['start'];
523
			$end = $this->recur['end'];
524
			$term = $this->recur['term'];
525
			$numocc = isset($this->recur['numoccur']) ? $this->recur['numoccur'] : false;
526
			$startocc = $this->recur['startocc'];
527
			$endocc = $this->recur['endocc'];
528
			$pattern = '';
529
			$occSingleDayRank = false;
530
			$occTimeRange = ($startocc != 0 && $endocc != 0);
531
532
			switch ($type) {
533
				// Daily
534
				case 0x0A:
535
					if ($everyn == 1) {
536
						$type = dgettext('zarafa', 'workday');
537
						$occSingleDayRank = true;
538
					}
539
					elseif ($everyn == (24 * 60)) {
540
						$type = dgettext('zarafa', 'day');
541
						$occSingleDayRank = true;
542
					}
543
					else {
544
						$everyn /= (24 * 60);
545
						$type = dgettext('zarafa', 'days');
546
						$occSingleDayRank = false;
547
					}
548
					break;
549
				// Weekly
550
				case 0x0B:
551
					if ($everyn == 1) {
552
						$type = dgettext('zarafa', 'week');
553
						$occSingleDayRank = true;
554
					}
555
					else {
556
						$type = dgettext('zarafa', 'weeks');
557
						$occSingleDayRank = false;
558
					}
559
					break;
560
				// Monthly
561
				case 0x0C:
562
					if ($everyn == 1) {
563
						$type = dgettext('zarafa', 'month');
564
						$occSingleDayRank = true;
565
					}
566
					else {
567
						$type = dgettext('zarafa', 'months');
568
						$occSingleDayRank = false;
569
					}
570
					break;
571
				// Yearly
572
				case 0x0D:
573
					if ($everyn <= 12) {
574
						$everyn = 1;
575
						$type = dgettext('zarafa', 'year');
576
						$occSingleDayRank = true;
577
					}
578
					else {
579
						$everyn = $everyn / 12;
580
						$type = dgettext('zarafa', 'years');
581
						$occSingleDayRank = false;
582
					}
583
					break;
584
			}
585
586
			// get timings of the first occurrence
587
			$firstoccstartdate = isset($startocc) ? $start + (((int) $startocc) * 60) : $start;
588
			$firstoccenddate = isset($endocc) ? $end + (((int) $endocc) * 60) : $end;
589
590
			$start = gmdate(dgettext('zarafa', 'd-m-Y'), $firstoccstartdate);
591
			$end = gmdate(dgettext('zarafa', 'd-m-Y'), $firstoccenddate);
592
			$startocc = gmdate(dgettext('zarafa', 'G:i'), $firstoccstartdate);
593
			$endocc = gmdate(dgettext('zarafa', 'G:i'), $firstoccenddate);
594
595
			// Based on the properties, we need to generate the recurrence pattern string.
596
			// This is obviously very easy since we can simply concatenate a bunch of strings,
597
			// however this messes up translations for languages which order their words
598
			// differently.
599
			// To improve translation quality we create a series of default strings, in which
600
			// we only have to fill in the correct variables. The base string is thus selected
601
			// based on the available properties.
602
			if ($term == 0x23) {
603
				// Never ends
604
				if ($occTimeRange) {
605
					if ($occSingleDayRank) {
606
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s from %s to %s.'), $type, $start, $startocc, $endocc);
607
					}
608
					else {
609
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s from %s to %s.'), $everyn, $type, $start, $startocc, $endocc);
610
					}
611
				}
612
				else {
613
					if ($occSingleDayRank) {
614
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s.'), $type, $start);
615
					}
616
					else {
617
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s.'), $everyn, $type, $start);
618
					}
619
				}
620
			}
621
			elseif ($term == 0x22) {
622
				// After a number of times
623
				if ($occTimeRange) {
624
					if ($occSingleDayRank) {
625
						$pattern = sprintf(dngettext(
626
							'zarafa',
627
							'Occurs every %s effective %s for %s occurrence from %s to %s.',
628
							'Occurs every %s effective %s for %s occurrences from %s to %s.',
629
							$numocc
0 ignored issues
show
Bug introduced by
It seems like $numocc can also be of type false; however, parameter $count of dngettext() 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

629
							/** @scrutinizer ignore-type */ $numocc
Loading history...
630
						), $type, $start, $numocc, $startocc, $endocc);
0 ignored issues
show
Bug introduced by
It seems like $numocc can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

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

630
						), $type, $start, /** @scrutinizer ignore-type */ $numocc, $startocc, $endocc);
Loading history...
631
					}
632
					else {
633
						$pattern = sprintf(dngettext(
634
							'zarafa',
635
							'Occurs every %s %s effective %s for %s occurrence from %s to %s.',
636
							'Occurs every %s %s effective %s for %s occurrences %s to %s.',
637
							$numocc
638
						), $everyn, $type, $start, $numocc, $startocc, $endocc);
639
					}
640
				}
641
				else {
642
					if ($occSingleDayRank) {
643
						$pattern = sprintf(dngettext(
644
							'zarafa',
645
							'Occurs every %s effective %s for %s occurrence.',
646
							'Occurs every %s effective %s for %s occurrences.',
647
							$numocc
648
						), $type, $start, $numocc);
649
					}
650
					else {
651
						$pattern = sprintf(dngettext(
652
							'zarafa',
653
							'Occurs every %s %s effective %s for %s occurrence.',
654
							'Occurs every %s %s effective %s for %s occurrences.',
655
							$numocc
656
						), $everyn, $type, $start, $numocc);
657
					}
658
				}
659
			}
660
			elseif ($term == 0x21) {
661
				// After the given enddate
662
				if ($occTimeRange) {
663
					if ($occSingleDayRank) {
664
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s until %s from %s to %s.'), $type, $start, $end, $startocc, $endocc);
665
					}
666
					else {
667
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s until %s from %s to %s.'), $everyn, $type, $start, $end, $startocc, $endocc);
668
					}
669
				}
670
				else {
671
					if ($occSingleDayRank) {
672
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s until %s.'), $type, $start, $end);
673
					}
674
					else {
675
						$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s until %s.'), $everyn, $type, $start, $end);
676
					}
677
				}
678
			}
679
680
			if (!empty($pattern)) {
681
				mapi_setprops($this->message, [$this->proptags["recurring_pattern"] => $pattern]);
682
			}
683
		}
684
685
		/*
686
		 * Remove an exception by base_date. This is the base date in local daystart time
687
		 */
688
		public function deleteException($base_date) {
689
			// Remove all exceptions on $base_date from the deleted and changed occurrences lists
690
691
			// Remove all items in $todelete from deleted_occurrences
692
			$new = [];
693
694
			foreach ($this->recur["deleted_occurrences"] as $entry) {
695
				if ($entry != $base_date) {
696
					$new[] = $entry;
697
				}
698
			}
699
			$this->recur["deleted_occurrences"] = $new;
700
701
			$new = [];
702
703
			foreach ($this->recur["changed_occurrences"] as $entry) {
704
				if (!$this->isSameDay($entry["basedate"], $base_date)) {
705
					$new[] = $entry;
706
				}
707
				else {
708
					$this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
0 ignored issues
show
Bug introduced by
$this->toGMT($this->tz, ...recur['startocc'] * 60) of type integer is incompatible with the type date expected by parameter $base_date of Recurrence::deleteExceptionAttachment(). ( Ignorable by Annotation )

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

708
					$this->deleteExceptionAttachment(/** @scrutinizer ignore-type */ $this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
Loading history...
709
				}
710
			}
711
712
			$this->recur["changed_occurrences"] = $new;
713
		}
714
715
		/**
716
		 * Function which saves the exception data in an attachment.
717
		 *
718
		 * @param array        $exception_props  the exception data (like any other MAPI appointment)
719
		 * @param array        $exception_recips list of recipients
720
		 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
721
		 *
722
		 * @return array properties of the exception
723
		 */
724
		public function createExceptionAttachment($exception_props, $exception_recips = [], $copy_attach_from = false) {
725
			// Create new attachment.
726
			$attachment = mapi_message_createattach($this->message);
727
			$props = [];
728
			$props[PR_ATTACHMENT_FLAGS] = 2;
729
			$props[PR_ATTACHMENT_HIDDEN] = true;
730
			$props[PR_ATTACHMENT_LINKID] = 0;
731
			$props[PR_ATTACH_FLAGS] = 0;
732
			$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
733
			$props[PR_DISPLAY_NAME] = "Exception";
734
			$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
735
			$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
736
			mapi_setprops($attachment, $props);
737
738
			$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
739
740
			if ($copy_attach_from) {
741
				$attachmentTable = mapi_message_getattachmenttable($copy_attach_from);
742
				if ($attachmentTable) {
743
					$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
744
745
					foreach ($attachments as $attach_props) {
746
						$attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
747
						$attach_newResourceMsg = mapi_message_createattach($imessage);
748
						mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
749
						mapi_savechanges($attach_newResourceMsg);
750
					}
751
				}
752
			}
753
754
			$props = $props + $exception_props;
755
756
			// FIXME: the following piece of code is written to fix the creation
757
			// of an exception. This is only a quickfix as it is not yet possible
758
			// to change an existing exception.
759
			// remove mv properties when needed
760
			foreach ($props as $propTag => $propVal) {
761
				if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)) {
762
					unset($props[$propTag]);
763
				}
764
			}
765
766
			mapi_setprops($imessage, $props);
767
768
			$this->setExceptionRecipients($imessage, $exception_recips, true);
769
770
			mapi_savechanges($imessage);
771
			mapi_savechanges($attachment);
772
		}
773
774
		/**
775
		 * Function which deletes the attachment of an exception.
776
		 *
777
		 * @param date $base_date base date of the attachment. Should be in GMT. The attachment
778
		 *                        actually saves the real time of the original date, so we have
779
		 *                        to check whether it's on the same day.
780
		 */
781
		public function deleteExceptionAttachment($base_date) {
782
			$attachments = mapi_message_getattachmenttable($this->message);
783
			// Retrieve only exceptions which are stored as embedded messages
784
			$attach_res = [
785
				RES_PROPERTY,
786
				[
787
					RELOP => RELOP_EQ,
788
					ULPROPTAG => PR_ATTACH_METHOD,
789
					VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
790
				],
791
			];
792
			$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
793
794
			foreach ($attachRows as $attachRow) {
795
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
796
				$exception = mapi_attach_openobj($tempattach);
797
798
				$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
799
800
				if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
0 ignored issues
show
Bug introduced by
$this->fromGMT($this->tz...>proptags['basedate']]) of type integer is incompatible with the type date expected by parameter $date of BaseRecurrence::dayStartOf(). ( Ignorable by Annotation )

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

800
				if ($this->dayStartOf(/** @scrutinizer ignore-type */ $this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
Loading history...
801
					mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
802
				}
803
			}
804
		}
805
806
		/**
807
		 * Function which deletes all attachments of a message.
808
		 */
809
		public function deleteAttachments() {
810
			$attachments = mapi_message_getattachmenttable($this->message);
811
			$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
812
813
			foreach ($attachTable as $attachRow) {
814
				if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
815
					mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
816
				}
817
			}
818
		}
819
820
		/**
821
		 * Get an exception attachment based on its basedate.
822
		 *
823
		 * @param mixed $base_date
824
		 */
825
		public function getExceptionAttachment($base_date) {
826
			// Retrieve only exceptions which are stored as embedded messages
827
			$attach_res = [
828
				RES_PROPERTY,
829
				[
830
					RELOP => RELOP_EQ,
831
					ULPROPTAG => PR_ATTACH_METHOD,
832
					VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
833
				],
834
			];
835
			$attachments = mapi_message_getattachmenttable($this->message);
836
			$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
837
838
			if (is_array($attachRows)) {
839
				foreach ($attachRows as $attachRow) {
840
					$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
841
					$exception = mapi_attach_openobj($tempattach);
842
843
					$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
844
845
					if (!isset($data[$this->proptags["basedate"]])) {
846
						// if no basedate found then it could be embedded message so ignore it
847
						// we need proper restriction to exclude embedded messages as well
848
						continue;
849
					}
850
851
					if ($this->isSameDay($this->fromGMT($this->tz, $data[$this->proptags["basedate"]]), $base_date)) {
852
						return $tempattach;
853
					}
854
				}
855
			}
856
857
			return false;
858
		}
859
860
		/**
861
		 * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met:
862
		 * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end>
863
		 * - The occurrence isn't specified as a deleted occurrence.
864
		 *
865
		 * @param array $items        reference to the array to be added to
866
		 * @param date  $start        start of timeframe in GMT TIME
867
		 * @param date  $end          end of timeframe in GMT TIME
868
		 * @param date  $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
869
		 * @param int   $startocc     start of occurrence since beginning of day in minutes
870
		 * @param int   $endocc       end of occurrence since beginning of day in minutes
871
		 * @param int   $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
872
		 * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
873
		 */
874
		public function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly) {
875
			$exception = $this->isException($basedate);
876
			if ($exception) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exception of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
877
				return false;
878
			}
879
			$occstart = $basedate + $startocc * 60;
880
			$occend = $basedate + $endocc * 60;
881
882
			// Convert to GMT
883
			$occstart = $this->toGMT($tz, $occstart);
884
			$occend = $this->toGMT($tz, $occend);
885
886
			/**
887
			 * FIRST PART: Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
888
			 * see any part of the appointment. Partial overlaps DO match.
889
			 *
890
			 * SECOND PART: check if occurrence is not a zero duration occurrence which
891
			 * starts at 00:00 and ends on 00:00. if it is so, then process
892
			 * the occurrence and send it in response.
893
			 */
894
			if (($occstart >= $end || $occend <= $start) && !($occstart == $occend && $occstart == $start)) {
895
				return;
896
			}
897
898
			// Properties for this occurrence are the same as the main object,
899
			// With these properties overridden
900
			$newitem = $this->messageprops;
901
			$newitem[$this->proptags["startdate"]] = $occstart;
902
			$newitem[$this->proptags["duedate"]] = $occend;
903
			$newitem[$this->proptags["commonstart"]] = $occstart;
904
			$newitem[$this->proptags["commonend"]] = $occend;
905
			$newitem["basedate"] = $basedate;
906
907
			// If reminderonly is set, only add reminders
908
			if ($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] == false)) {
909
				return;
910
			}
911
912
			$items[] = $newitem;
913
		}
914
915
		/**
916
		 * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe.
917
		 *
918
		 * @param array $items reference to the array to be added to
919
		 * @param date  $start start of timeframe in GMT TIME
920
		 * @param date  $end   end of timeframe in GMT TIME
921
		 */
922
		public function processExceptionItems(&$items, $start, $end) {
923
			$limit = 0;
924
			foreach ($this->recur["changed_occurrences"] as $exception) {
925
				// Convert to GMT
926
				$occstart = $this->toGMT($this->tz, $exception["start"]);
927
				$occend = $this->toGMT($this->tz, $exception["end"]);
928
929
				// Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
930
				// see any part of the appointment. Partial overlaps DO match.
931
				if ($occstart >= $end || $occend <= $start) {
932
					continue;
933
				}
934
935
				array_push($items, $this->getExceptionProperties($exception));
936
				if (count($items) == $limit) {
937
					break;
938
				}
939
			}
940
		}
941
942
		/**
943
		 * Function which verifies if on the given date an exception, delete or change, occurs.
944
		 *
945
		 * @param date  $date     the date
946
		 * @param mixed $basedate
947
		 *
948
		 * @return array the exception, true - if an occurrence is deleted on the given date, false - no exception occurs on the given date
949
		 */
950
		public function isException($basedate) {
951
			if ($this->isDeleteException($basedate)) {
952
				return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type array.
Loading history...
953
			}
954
955
			if ($this->getChangeException($basedate) != false) {
956
				return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type array.
Loading history...
957
			}
958
959
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
960
		}
961
962
		/**
963
		 * Returns TRUE if there is a DELETE exception on the given base date.
964
		 *
965
		 * @param mixed $basedate
966
		 */
967
		public function isDeleteException($basedate) {
968
			// Check if the occurrence is deleted on the specified date
969
			foreach ($this->recur["deleted_occurrences"] as $deleted) {
970
				if ($this->isSameDay($deleted, $basedate)) {
971
					return true;
972
				}
973
			}
974
975
			return false;
976
		}
977
978
		/**
979
		 * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise.
980
		 *
981
		 * @param mixed $basedate
982
		 */
983
		public function getChangeException($basedate) {
984
			// Check if the occurrence is modified on the specified date
985
			foreach ($this->recur["changed_occurrences"] as $changed) {
986
				if ($this->isSameDay($changed["basedate"], $basedate)) {
987
					return $changed;
988
				}
989
			}
990
991
			return false;
992
		}
993
994
		/**
995
		 * Function to see if two dates are on the same day.
996
		 *
997
		 * @param date  $time1 date 1
998
		 * @param date  $time2 date 2
999
		 * @param mixed $date1
1000
		 * @param mixed $date2
1001
		 *
1002
		 * @return bool Returns TRUE when both dates are on the same day
1003
		 */
1004
		public function isSameDay($date1, $date2) {
1005
			$time1 = $this->gmtime($date1);
1006
			$time2 = $this->gmtime($date2);
1007
1008
			return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"];
1009
		}
1010
1011
		/**
1012
		 * Function to get all properties of a single changed exception.
1013
		 *
1014
		 * @param date  $date      base date of exception
1015
		 * @param mixed $exception
1016
		 *
1017
		 * @return array associative array of properties for the exception, compatible with
1018
		 */
1019
		public function getExceptionProperties($exception) {
1020
			// Exception has same properties as main object, with some properties overridden:
1021
			$item = $this->messageprops;
1022
1023
			// Special properties
1024
			$item["exception"] = true;
1025
			$item["basedate"] = $exception["basedate"]; // note that the basedate is always in local time !
1026
1027
			// MAPI-compatible properties (you can handle an exception as a normal calendar item like this)
1028
			$item[$this->proptags["startdate"]] = $this->toGMT($this->tz, $exception["start"]);
1029
			$item[$this->proptags["duedate"]] = $this->toGMT($this->tz, $exception["end"]);
1030
			$item[$this->proptags["commonstart"]] = $item[$this->proptags["startdate"]];
1031
			$item[$this->proptags["commonend"]] = $item[$this->proptags["duedate"]];
1032
1033
			if (isset($exception["subject"])) {
1034
				$item[$this->proptags["subject"]] = $exception["subject"];
1035
			}
1036
1037
			if (isset($exception["label"])) {
1038
				$item[$this->proptags["label"]] = $exception["label"];
1039
			}
1040
1041
			if (isset($exception["alldayevent"])) {
1042
				$item[$this->proptags["alldayevent"]] = $exception["alldayevent"];
1043
			}
1044
1045
			if (isset($exception["location"])) {
1046
				$item[$this->proptags["location"]] = $exception["location"];
1047
			}
1048
1049
			if (isset($exception["remind_before"])) {
1050
				$item[$this->proptags["reminder_minutes"]] = $exception["remind_before"];
1051
			}
1052
1053
			if (isset($exception["reminder_set"])) {
1054
				$item[$this->proptags["reminder"]] = $exception["reminder_set"];
1055
			}
1056
1057
			if (isset($exception["busystatus"])) {
1058
				$item[$this->proptags["busystatus"]] = $exception["busystatus"];
1059
			}
1060
1061
			return $item;
1062
		}
1063
1064
		/**
1065
		 * Function which sets recipients for an exception.
1066
		 *
1067
		 * The $exception_recips can be provided in 2 ways:
1068
		 *  - A delta which indicates which recipients must be added, removed or deleted.
1069
		 *  - A complete array of the recipients which should be applied to the message.
1070
		 *
1071
		 * The first option is preferred as it will require less work to be executed.
1072
		 *
1073
		 * @param resource $message          exception attachment of recurring item
1074
		 * @param array    $exception_recips list of recipients
1075
		 * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1076
		 *                                   message to the attachment by default. False if only the $exception_recips changes should
1077
		 *                                   be applied.
1078
		 */
1079
		public function setExceptionRecipients($message, $exception_recips, $copy_orig_recips = true) {
1080
			if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) {
1081
				$this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips);
1082
			}
1083
			else {
1084
				$this->setAllExceptionRecipients($message, $exception_recips);
1085
			}
1086
		}
1087
1088
		/**
1089
		 * Function which applies the provided delta for recipients changes to the exception.
1090
		 *
1091
		 * The $exception_recips should be an array containing the following keys:
1092
		 *  - "add": this contains an array of recipients which must be added
1093
		 *  - "remove": This contains an array of recipients which must be removed
1094
		 *  - "modify": This contains an array of recipients which must be modified
1095
		 *
1096
		 * @param resource $message          exception attachment of recurring item
1097
		 * @param array    $exception_recips list of recipients
1098
		 * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1099
		 *                                   message to the attachment by default. False if only the $exception_recips changes should
1100
		 *                                   be applied.
1101
		 * @param mixed    $exception
1102
		 */
1103
		public function setDeltaExceptionRecipients($exception, $exception_recips, $copy_orig_recips) {
1104
			// Check if the recipients from the original message should be copied,
1105
			// if so, open the recipient table of the parent message and apply all
1106
			// rows on the target recipient.
1107
			if ($copy_orig_recips === true) {
1108
				$origTable = mapi_message_getrecipienttable($this->message);
1109
				$recipientRows = mapi_table_queryallrows($origTable, $this->recipprops);
1110
				mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
1111
			}
1112
1113
			// Add organizer to meeting only if it is not organized.
1114
			$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']]);
1115
			if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1116
				$this->addOrganizer($msgprops, $exception_recips['add']);
1117
			}
1118
1119
			// Remove all deleted recipients
1120
			if (isset($exception_recips['remove'])) {
1121
				foreach ($exception_recips['remove'] as &$recip) {
1122
					if (!isset($recip[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1123
						$recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1124
					}
1125
					else {
1126
						$recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1127
					}
1128
					$recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone;		// No Response required
1129
				}
1130
				unset($recip);
1131
				mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']);
1132
			}
1133
1134
			// Add all new recipients
1135
			if (isset($exception_recips['add'])) {
1136
				mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']);
1137
			}
1138
1139
			// Modify the existing recipients
1140
			if (isset($exception_recips['modify'])) {
1141
				mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']);
1142
			}
1143
		}
1144
1145
		/**
1146
		 * Function which applies the provided recipients to the exception, also checks for deleted recipients.
1147
		 *
1148
		 * The $exception_recips should be an array containing all recipients which must be applied
1149
		 * to the exception. This will copy all recipients from the original message and then start filter
1150
		 * out all recipients which are not provided by the $exception_recips list.
1151
		 *
1152
		 * @param resource $message          exception attachment of recurring item
1153
		 * @param array    $exception_recips list of recipients
1154
		 */
1155
		public function setAllExceptionRecipients($message, $exception_recips) {
1156
			$deletedRecipients = [];
1157
			$useMessageRecipients = false;
1158
1159
			$recipientTable = mapi_message_getrecipienttable($message);
1160
			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1161
1162
			if (empty($recipientRows)) {
1163
				$useMessageRecipients = true;
1164
				$recipientTable = mapi_message_getrecipienttable($this->message);
1165
				$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1166
			}
1167
1168
			// Add organizer to meeting only if it is not organized.
1169
			$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']]);
1170
			if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1171
				$this->addOrganizer($msgprops, $exception_recips);
1172
			}
1173
1174
			if (!empty($exception_recips)) {
1175
				foreach ($recipientRows as $key => $recipient) {
1176
					$found = false;
1177
					foreach ($exception_recips as $excep_recip) {
1178
						if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1179
							$found = true;
1180
						}
1181
					}
1182
1183
					if (!$found) {
1184
						$foundInDeletedRecipients = false;
1185
						// Look if the $recipient is in the list of deleted recipients
1186
						if (!empty($deletedRecipients)) {
1187
							foreach ($deletedRecipients as $recip) {
1188
								if ($recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1189
									$foundInDeletedRecipients = true;
1190
									break;
1191
								}
1192
							}
1193
						}
1194
1195
						// If recipient is not in list of deleted recipient, add him
1196
						if (!$foundInDeletedRecipients) {
1197
							if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1198
								$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1199
							}
1200
							else {
1201
								$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1202
							}
1203
							$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;	// No Response required
1204
							$deletedRecipients[] = $recipient;
1205
						}
1206
					}
1207
1208
					// When $message contains a non-empty recipienttable, we must delete the recipients
1209
					// before re-adding them. However, when $message is doesn't contain any recipients,
1210
					// we are using the recipient table of the original message ($this->message)
1211
					// rather then $message. In that case, we don't need to remove the recipients
1212
					// from the $message, as the recipient table is already empty, and
1213
					// mapi_message_modifyrecipients() will throw an error.
1214
					if ($useMessageRecipients === false) {
1215
						mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
1216
					}
1217
				}
1218
				$exception_recips = array_merge($exception_recips, $deletedRecipients);
1219
			}
1220
			else {
1221
				$exception_recips = $recipientRows;
1222
			}
1223
1224
			if (!empty($exception_recips)) {
1225
				// Set the new list of recipients on the exception message, this also removes the existing recipients
1226
				mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $exception_recips);
1227
			}
1228
		}
1229
1230
		/**
1231
		 * Function returns basedates of all changed occurrences.
1232
		 *
1233
		 *@return array array(
1234
		 *					0 => 123459321
1235
		 *				)
1236
		 */
1237
		public function getAllExceptions() {
1238
			$result = false;
1239
			if (!empty($this->recur["changed_occurrences"])) {
1240
				$result = [];
1241
				foreach ($this->recur["changed_occurrences"] as $exception) {
1242
					$result[] = $exception["basedate"];
1243
				}
1244
1245
				return $result;
1246
			}
1247
1248
			return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type false which is incompatible with the documented return type array.
Loading history...
1249
		}
1250
1251
		/**
1252
		 *  Function which adds organizer to recipient list which is passed.
1253
		 *  This function also checks if it has organizer.
1254
		 *
1255
		 * @param array $messageProps message properties
1256
		 * @param array $recipients   recipients list of message
1257
		 * @param bool  $isException  true if we are processing recipient of exception
1258
		 */
1259
		public function addOrganizer($messageProps, &$recipients, $isException = false) {
1260
			$hasOrganizer = false;
1261
			// Check if meeting already has an organizer.
1262
			foreach ($recipients as $key => $recipient) {
1263
				if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
1264
					$hasOrganizer = true;
1265
				}
1266
				elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
1267
					// Recipients for an occurrence
1268
					$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
1269
				}
1270
			}
1271
1272
			if (!$hasOrganizer) {
1273
				// Create organizer.
1274
				$organizer = [];
1275
				$organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
1276
				$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1277
				$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1278
				$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
1279
				$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1280
				$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
1281
				$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
1282
				$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
1283
				$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
1284
1285
				// Add organizer to recipients list.
1286
				array_unshift($recipients, $organizer);
1287
			}
1288
		}
1289
	}
1290
1291
	/*
1292
1293
	From http://www.ohelp-one.com/new-6765483-3268.html:
1294
1295
	Recurrence Data Structure Offset Type Value
1296
1297
	0 ULONG (?) Constant : { 0x04, 0x30, 0x04, 0x30}
1298
1299
	4 UCHAR 0x0A + recurrence type: 0x0A for daily, 0x0B for weekly, 0x0C for
1300
	monthly, 0x0D for yearly
1301
1302
	5 UCHAR Constant: { 0x20}
1303
1304
	6 ULONG Seems to be a variant of the recurrence type: 1 for daily every n
1305
	days, 2 for daily every weekday and weekly, 3 for monthly or yearly. The
1306
	special exception is regenerating tasks that regenerate on a weekly basis: 0
1307
	is used in that case (I have no idea why).
1308
1309
	Here's the recurrence-type-specific data. Because the daily every N days
1310
	data are 4 bytes shorter than the data for the other types, the offsets for
1311
	the rest of the data will be 4 bytes off depending on the recurrence type.
1312
1313
	Daily every N days:
1314
1315
	10 ULONG ( N - 1) * ( 24 * 60). I'm not sure what this is used for, but it's consistent.
1316
1317
	14 ULONG N * 24 * 60: minutes between recurrences
1318
1319
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1320
	regenerating tasks.
1321
1322
	Daily every weekday (this is essentially a subtype of weekly recurrence):
1323
1324
	10 ULONG 6 * 24 * 60: minutes between recurrences ( a week... sort of)
1325
1326
	14 ULONG 1: recur every week (corresponds to the second parameter for weekly
1327
	recurrence)
1328
1329
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1330
	regenerating tasks.
1331
1332
	22 ULONG 0x3E: bitmask for recurring every weekday (corresponds to fourth
1333
	parameter for weekly recurrence)
1334
1335
	Weekly every N weeks for all events and non-regenerating tasks:
1336
1337
	10 ULONG 6 * 24 * 60: minutes between recurrences (a week... sort of)
1338
1339
	14 ULONG N: recurrence interval
1340
1341
	18 ULONG Constant: 0
1342
1343
	22 ULONG Bitmask for determining which days of the week the event recurs on
1344
	( 1 << dayOfWeek, where Sunday is 0).
1345
1346
	Weekly every N weeks for regenerating tasks: 10 ULONG Constant: 0
1347
1348
	14 ULONG N * 7 * 24 * 60: recurrence interval in minutes between occurrences
1349
1350
	18 ULONG Constant: 1
1351
1352
	Monthly every N months on day D:
1353
1354
	10 ULONG This is the most complicated value
1355
	in the entire mess. It's basically a very complicated way of stating the
1356
	recurrence interval. I tweaked fbs' basic algorithm. DateTime::MonthInDays
1357
	simply returns the number of days in a given month, e.g. 31 for July for 28
1358
	for February (the algorithm doesn't take into account leap years, but it
1359
	doesn't seem to matter). My DateTime object, like Microsoft's COleDateTime,
1360
	uses 1-based months (i.e. January is 1, not 0). With that in mind, this
1361
	works:
1362
1363
	long monthIndex = ( ( ( ( 12 % schedule-=GetInterval()) *
1364
1365
	( ( schedule-=GetStartDate().GetYear() - 1601) %
1366
1367
	schedule-=GetInterval())) % schedule-=GetInterval()) +
1368
1369
	( schedule-=GetStartDate().GetMonth() - 1)) % schedule-=GetInterval();
1370
1371
	for( int i = 0; i < monthIndex; i++)
1372
1373
	{
1374
1375
	value += DateTime::GetDaysInMonth( ( i % 12) + 1) * 24 * 60;
1376
1377
	}
1378
1379
	This should work for any recurrence interval, including those greater than
1380
	12.
1381
1382
	14 ULONG N: recurrence interval
1383
1384
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1385
	regenerating tasks.
1386
1387
	22 ULONG D: day of month the event recurs on (if this value is greater than
1388
	the number of days in a given month [e.g. 31 for and recurs in June], then
1389
	the event will recur on the last day of the month)
1390
1391
	Monthly every N months on the Xth Y (e.g. "2nd Tuesday"):
1392
1393
	10 ULONG See above: same as for monthly every N months on day D
1394
1395
	14 ULONG N: recurrence interval
1396
1397
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1398
	regenerating tasks.
1399
1400
	22 ULONG Y: bitmask for determining which day of the week the event recurs
1401
	on (see weekly every N weeks). Some useful values are 0x7F for any day, 0x3E
1402
	for a weekday, or 0x41 for a weekend day.
1403
1404
	26 ULONG X: 1 for first occurrence, 2 for second, etc. 5 for last
1405
	occurrence. E.g. for "2nd Tuesday", you should have values of 0x04 for the
1406
	prior value and 2 for this one.
1407
1408
	Yearly on day D of month M:
1409
1410
	10 ULONG M (sort of): This is another messy
1411
	value. It's the number of minute since the startning of the year to the
1412
	given month. For an explanation of GetDaysInMonth, see monthly every N
1413
	months. This will work:
1414
1415
	ULONG monthOfYearInMinutes = 0;
1416
1417
	for( int i = DateTime::cJanuary; i < schedule-=GetMonth(); i++)
1418
1419
	{
1420
1421
	monthOfYearInMinutes += DateTime::GetDaysInMonth( i) * 24 * 60;
1422
1423
	}
1424
1425
1426
1427
	14 ULONG 12: recurrence interval in months. Naturally, 12.
1428
1429
	18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1430
	regenerating tasks.
1431
1432
	22 ULONG D: day of month the event recurs on. See monthly every N months on
1433
	day D.
1434
1435
	Yearly on the Xth Y of month M: 10 ULONG M (sort of): See yearly on day D of
1436
	month M.
1437
1438
	14 ULONG 12: recurrence interval in months. Naturally, 12.
1439
1440
	18 ULONG Constant: 0
1441
1442
	22 ULONG Y: see monthly every N months on the Xth Y.
1443
1444
	26 ULONG X: see monthly every N months on the Xth Y.
1445
1446
	After these recurrence-type-specific values, the offsets will change
1447
	depending on the type. For every type except daily every N days, the offsets
1448
	will grow by at least 4. For those types using the Xth Y, the offsets will
1449
	grow by an additional 4, for a total of 8. The offsets for the rest of these
1450
	values will be given for the most basic case, daily every N days, i.e.
1451
	without any growth. Adjust as necessary. Also, the presence of exceptions
1452
	will change the offsets following the exception data by a variable number of
1453
	bytes, so the offsets given in the table are accurate only for those
1454
	recurrence patterns without any exceptions.
1455
1456
1457
	22 UCHAR Type of pattern termination: 0x21 for terminating on a given date, 0x22 for terminating
1458
	after a given number of recurrences, or 0x23 for never terminating
1459
	(recurring infinitely)
1460
1461
	23 UCHARx3 Constant: { 0x20, 0x00, 0x00}
1462
1463
	26 ULONG Number of occurrences in pattern: 0 for infinite recurrence,
1464
	otherwise supply the value, even if it terminates on a given date, not after
1465
	a given number
1466
1467
	30 ULONG Constant: 0
1468
1469
	34 ULONG Number of exceptions to pattern (i.e. deleted or changed
1470
	occurrences)
1471
1472
	.... ULONGxN Base date of each exception, given in hundreds of nanoseconds
1473
	since 1601, so see below to turn them into a comprehensible format. The base
1474
	date of an exception is the date (and only the date-- not the time) the
1475
	exception would have occurred on in the pattern. They must occur in
1476
	ascending order.
1477
1478
	38 ULONG Number of changed exceptions (i.e. total number of exceptions -
1479
	number of deleted exceptions): if there are changed exceptions, again, more
1480
	data will be needed, but that will wait
1481
1482
	.... ULONGxN Start date (and only the date-- not the time) of each changed
1483
	exception, i.e. the exceptions which aren't deleted. These must also occur
1484
	in ascending order. If all of the exceptions are deleted, this data will be
1485
	absent. If present, they will be in the format above. Any dates that are in
1486
	the first list but not in the second are exceptions that have been deleted
1487
	(i.e. the difference between the two sets). Note that this is the start date
1488
	(including time), not the base date. Given that the values are unordered and
1489
	that they can't be matched up against the previous list in this iteration of
1490
	the recurrence data (they could in previous ones), it is very difficult to
1491
	tell which exceptions are deleted and which are changed. Fortunately, for
1492
	this new format, the base dates are given on the attachment representing the
1493
	changed exception (described below), so you can simply ignore this list of
1494
	changed exceptions. Just create a list of exceptions from the previous list
1495
	and assume they're all deleted unless you encounter an attachment with a
1496
	matching base date later on.
1497
1498
	42 ULONG Start date of pattern given in hundreds of nanoseconds since 1601;
1499
	see below for an explanation.
1500
1501
	46 ULONG End date of pattern: see start date of pattern
1502
1503
	50 ULONG Constant: { 0x06, 0x30, 0x00, 0x00}
1504
1505
	NOTE: I find the following 8-byte sequence of bytes to be very useful for
1506
	orienting myself when looking at the raw data. If you can find { 0x06, 0x30,
1507
	0x00, 0x00, 0x08, 0x30, 0x00, 0x00}, you can use these tables to work either
1508
	forwards or backwards to find the data you need. The sequence sort of
1509
	delineates certain critical exception-related data and delineates the
1510
	exceptions themselves from the rest of the data and is relatively easy to
1511
	find. If you're going to be meddling in here a lot, I suggest making a
1512
	friend of ol' 0x00003006.
1513
1514
	54 UCHAR This number is some kind of version indicator. Use 0x08 for Outlook
1515
	2003. I believe 0x06 is Outlook 2000 and possibly 98, while 0x07 is Outlook
1516
	XP. This number must be consistent with the features of the data structure
1517
	generated by the version of Outlook indicated thereby-- there are subtle
1518
	differences between the structures, and, if the version doesn't match the
1519
	data, Outlook will sometimes failto read the structure.
1520
1521
	55 UCHARx3 Constant: { 0x30, 0x00, 0x00}
1522
1523
	58 ULONG Start time of occurrence in minutes: e.g. 0 for midnight or 720 for
1524
	12 PM
1525
1526
	62 ULONG End time of occurrence in minutes: i.e. start time + duration, e.g.
1527
	900 for an event that starts at 12 PM and ends at 3PM
1528
1529
	Exception Data 66 USHORT Number of changed exceptions: essentially a check
1530
	on the prior occurrence of this value; should be equivalent.
1531
1532
	NOTE: The following structure will occur N many times (where N = number of
1533
	changed exceptions), and each structure can be of variable length.
1534
1535
	.... ULONG Start date of changed exception given in hundreds of nanoseconds
1536
	since 1601
1537
1538
	.... ULONG End date of changed exception given in hundreds of nanoseconds
1539
	since 1601
1540
1541
	.... ULONG This is a value I don't clearly understand. It seems to be some
1542
	kind of archival value that matches the start time most of the time, but
1543
	will lag behind when the start time is changed and then match up again under
1544
	certain conditions later. In any case, setting to the same value as the
1545
	start time seems to work just fine (more information on this value would be
1546
	appreciated).
1547
1548
	.... USHORT Bitmask of changes to the exception (see below). This will be 0
1549
	if the only changes to the exception were to its start or end time.
1550
1551
	.... ULONGxN Numeric values (e.g. label or minutes to remind before the
1552
	event) changed in the exception. These will occur in the order of their
1553
	corresponding bits (see below). If no numeric values were changed, then
1554
	these values will be absent.
1555
1556
	NOTE: The following three values constitute a single sub-structure that will
1557
	occur N many times, where N is the number of strings that are changed in the
1558
	exception. Since there are at most 2 string values that can be excepted
1559
	(i.e. subject [or description], and location), there can at most be two of
1560
	these, but there may be none.
1561
1562
	.... USHORT Length of changed string value with NULL character
1563
1564
	.... USHORT Length of changed string value without NULL character (i.e.
1565
	previous value - 1)
1566
1567
	.... CHARxN Changed string value (without NULL terminator)
1568
1569
	Unicode Data NOTE: If a string value was changed on an exception, those
1570
	changed string values will reappear here in Unicode format after 8 bytes of
1571
	NULL padding (possibly a Unicode terminator?). For each exception with a
1572
	changed string value, there will be an identifier, followed by the changed
1573
	strings in Unicode. The strings will occur in the order of their
1574
	corresponding bits (see below). E.g., if both subject and location were
1575
	changed in the exception, there would be the 3-ULONG identifier, then the
1576
	length of the subject, then the subject, then the length of the location,
1577
	then the location.
1578
1579
	70 ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}. This
1580
	padding serves as a barrier between the older data structure and the
1581
	appended Unicode data. This is the same sequence as the Unicode terminator,
1582
	but I'm not sure whether that's its identity or not.
1583
1584
	.... ULONGx3 These are the three times used to identify the exception above:
1585
	start date, end date, and repeated start date. These should be the same as
1586
	they were above.
1587
1588
	.... USHORT Length of changed string value without NULL character. This is
1589
	given as count of WCHARs, so it should be identical to the value above.
1590
1591
	.... WCHARxN Changed string value in Unicode (without NULL terminator)
1592
1593
	Terminator ... ULONGxN Constant: { 0x00, 0x00, 0x00, 0x00}. 4 bytes of NULL
1594
	padding per changed exception. If there were no changed exceptions, all
1595
	you'll need is the final terminator below.
1596
1597
	.... ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}.
1598
1599
	*/
1600