Passed
Push — master ( f66da6...7a87af )
by
unknown
02:36 queued 15s
created

class.recurrence.php (35 issues)

Labels
Severity
1
<?php
2
/*
3
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH
6
 */
7
8
/**
9
 * Recurrence.
10
 */
11
class Recurrence extends BaseRecurrence {
12
	/*
13
	 * ABOUT TIMEZONES
14
	 *
15
	 * Timezones are rather complicated here so here are some rules to think about:
16
	 *
17
	 * - Timestamps in mapi-like properties (so in PT_SYSTIME properties) are always in GMT (including
18
	 *   the 'basedate' property in exceptions !!)
19
	 * - Timestamps for recurrence (so start/end of recurrence, and basedates for exceptions (everything
20
	 *   outside the 'basedate' property in the exception !!), and start/endtimes for exceptions) are
21
	 *   always in LOCAL time.
22
	 */
23
24
	// All properties for a recipient that are interesting
25
	public $recipprops = [
26
		PR_ENTRYID,
27
		PR_SEARCH_KEY,
28
		PR_DISPLAY_NAME,
29
		PR_EMAIL_ADDRESS,
30
		PR_RECIPIENT_ENTRYID,
31
		PR_RECIPIENT_TYPE,
32
		PR_SEND_INTERNET_ENCODING,
33
		PR_SEND_RICH_INFO,
34
		PR_RECIPIENT_DISPLAY_NAME,
35
		PR_ADDRTYPE,
36
		PR_DISPLAY_TYPE,
37
		PR_DISPLAY_TYPE_EX,
38
		PR_RECIPIENT_TRACKSTATUS,
39
		PR_RECIPIENT_TRACKSTATUS_TIME,
40
		PR_RECIPIENT_FLAGS,
41
		PR_ROWID,
42
	];
43
44
	/**
45
	 * Constructor.
46
	 *
47
	 * @param resource $store    MAPI Message Store Object
48
	 * @param mixed    $message  the MAPI (appointment) message
49
	 * @param array    $proptags an associative array of protags and their values
50
	 */
51
	public function __construct($store, $message, $proptags = []) {
52
		if (!empty($proptags)) {
53
			$this->proptags = $proptags;
54
		}
55
		else {
56
			$properties = [];
57
			$properties["entryid"] = PR_ENTRYID;
58
			$properties["parent_entryid"] = PR_PARENT_ENTRYID;
59
			$properties["message_class"] = PR_MESSAGE_CLASS;
60
			$properties["icon_index"] = PR_ICON_INDEX;
61
			$properties["subject"] = PR_SUBJECT;
62
			$properties["display_to"] = PR_DISPLAY_TO;
63
			$properties["importance"] = PR_IMPORTANCE;
64
			$properties["sensitivity"] = PR_SENSITIVITY;
65
			$properties["startdate"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidAppointmentStartWhole;
66
			$properties["duedate"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidAppointmentEndWhole;
67
			$properties["recurring"] = "PT_BOOLEAN:PSETID_Appointment:" . PidLidRecurring;
68
			$properties["recurring_data"] = "PT_BINARY:PSETID_Appointment:" . PidLidAppointmentRecur;
69
			$properties["busystatus"] = "PT_LONG:PSETID_Appointment:" . PidLidBusyStatus;
70
			$properties["label"] = "PT_LONG:PSETID_Appointment:0x8214";
71
			$properties["alldayevent"] = "PT_BOOLEAN:PSETID_Appointment:" . PidLidAppointmentSubType;
72
			$properties["private"] = "PT_BOOLEAN:PSETID_Common:" . PidLidPrivate;
73
			$properties["meeting"] = "PT_LONG:PSETID_Appointment:" . PidLidAppointmentStateFlags;
74
			$properties["startdate_recurring"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidClipStart;
75
			$properties["enddate_recurring"] = "PT_SYSTIME:PSETID_Appointment:" . PidLidClipEnd;
76
			$properties["recurring_pattern"] = "PT_STRING8:PSETID_Appointment: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 mixed $base_date        the base date of the exception (LOCAL time of non-exception occurrence)
107
	 * @param bool  $delete           true - delete occurrence, false - create new exception or modify existing
108
	 * @param array $exception_recips list of recipients
109
	 * @param mixed $copy_attach_from mapi message from which attachments should be copied
110
	 */
111
	public function createException($exception_props, $base_date, $delete = false, $exception_recips = [], $copy_attach_from = false): bool {
112
		$baseday = $this->dayStartOf($base_date);
113
		$basetime = $baseday + $this->recur["startocc"] * 60;
114
115
		// Remove any pre-existing exception on this base date
116
		if ($this->isException($baseday)) {
117
			$this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence).
118
		}
119
120
		if (!$delete) {
121
			if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
122
				return false;
123
			}
124
			// Properties in the attachment are the properties of the base object, plus $exception_props plus the base date
125
			foreach (["subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus"] as $propname) {
126
				if (isset($this->messageprops[$this->proptags[$propname]])) {
127
					$props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]];
128
				}
129
			}
130
131
			$props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
132
			unset($exception_props[PR_MESSAGE_CLASS], $exception_props[PR_ICON_INDEX]);
133
134
			$props = $exception_props + $props;
135
136
			// Basedate in the exception attachment is the GMT time at which the original occurrence would have been
137
			$props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime);
138
139
			if (!isset($exception_props[$this->proptags["startdate"]])) {
140
				$props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date);
141
			}
142
143
			if (!isset($exception_props[$this->proptags["duedate"]])) {
144
				$props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date);
145
			}
146
147
			// synchronize commonstart/commonend with startdate/duedate
148
			if (isset($props[$this->proptags["startdate"]])) {
149
				$props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]];
150
			}
151
152
			if (isset($props[$this->proptags["duedate"]])) {
153
				$props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]];
154
			}
155
156
			// Save the data into an attachment
157
			$this->createExceptionAttachment($props, $exception_recips, $copy_attach_from);
158
159
			$changed_item = [];
160
161
			$changed_item["basedate"] = $basetime;
162
			$changed_item["start"] = $this->fromGMT($this->tz, $props[$this->proptags["startdate"]]);
163
			$changed_item["end"] = $this->fromGMT($this->tz, $props[$this->proptags["duedate"]]);
164
165
			if (array_key_exists($this->proptags["subject"], $exception_props)) {
166
				$changed_item["subject"] = $exception_props[$this->proptags["subject"]];
167
			}
168
169
			if (array_key_exists($this->proptags["location"], $exception_props)) {
170
				$changed_item["location"] = $exception_props[$this->proptags["location"]];
171
			}
172
173
			if (array_key_exists($this->proptags["label"], $exception_props)) {
174
				$changed_item["label"] = $exception_props[$this->proptags["label"]];
175
			}
176
177
			if (array_key_exists($this->proptags["reminder"], $exception_props)) {
178
				$changed_item["reminder_set"] = $exception_props[$this->proptags["reminder"]];
179
			}
180
181
			if (array_key_exists($this->proptags["reminder_minutes"], $exception_props)) {
182
				$changed_item["remind_before"] = $exception_props[$this->proptags["reminder_minutes"]];
183
			}
184
185
			if (array_key_exists($this->proptags["alldayevent"], $exception_props)) {
186
				$changed_item["alldayevent"] = $exception_props[$this->proptags["alldayevent"]];
187
			}
188
189
			if (array_key_exists($this->proptags["busystatus"], $exception_props)) {
190
				$changed_item["busystatus"] = $exception_props[$this->proptags["busystatus"]];
191
			}
192
193
			// Add the changed occurrence to the list
194
			array_push($this->recur["changed_occurrences"], $changed_item);
195
		}
196
		else {
197
			// Delete the occurrence by placing it in the deleted occurrences list
198
			array_push($this->recur["deleted_occurrences"], $baseday);
199
		}
200
201
		// Turn on hideattachments, because the attachments in this item are the exceptions
202
		mapi_setprops($this->message, [$this->proptags["hideattachments"] => true]);
0 ignored issues
show
The function mapi_setprops 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

202
		/** @scrutinizer ignore-call */ 
203
  mapi_setprops($this->message, [$this->proptags["hideattachments"] => true]);
Loading history...
203
204
		// Save recurrence data to message
205
		$this->saveRecurrence();
206
207
		return true;
208
	}
209
210
	/**
211
	 * Modifies an existing exception, but only updates the given properties
212
	 * NOTE: You can't remove properties from an exception, only add new ones.
213
	 *
214
	 * @param mixed $exception_props
215
	 * @param mixed $base_date
216
	 * @param mixed $exception_recips
217
	 * @param mixed $copy_attach_from
218
	 */
219
	public function modifyException($exception_props, $base_date, $exception_recips = [], $copy_attach_from = false): bool {
220
		if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
221
			return false;
222
		}
223
224
		$baseday = $this->dayStartOf($base_date);
225
		$extomodify = false;
226
227
		for ($i = 0, $len = count($this->recur["changed_occurrences"]); $i < $len; ++$i) {
228
			if ($this->isSameDay($this->recur["changed_occurrences"][$i]["basedate"], $baseday)) {
229
				$extomodify = &$this->recur["changed_occurrences"][$i];
230
			}
231
		}
232
233
		if (!$extomodify) {
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);
0 ignored issues
show
The function mapi_attach_openobj 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

299
			$message = /** @scrutinizer ignore-call */ mapi_attach_openobj($attach, MAPI_MODIFY);
Loading history...
300
301
			// Set exception properties on embedded message and save
302
			mapi_setprops($message, $exception_props);
0 ignored issues
show
The function mapi_setprops 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

302
			/** @scrutinizer ignore-call */ 
303
   mapi_setprops($message, $exception_props);
Loading history...
303
			$this->setExceptionRecipients($message, $exception_recips, false);
304
			mapi_savechanges($message);
0 ignored issues
show
The function mapi_savechanges 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

304
			/** @scrutinizer ignore-call */ 
305
   mapi_savechanges($message);
Loading history...
305
306
			// If a new start or duedate is provided, we update the properties 'PR_EXCEPTION_STARTTIME' and 'PR_EXCEPTION_ENDTIME'
307
			// on the attachment which holds the embedded msg and save everything.
308
			$props = [];
309
			if (isset($exception_props[$this->proptags["startdate"]])) {
310
				$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
311
			}
312
			if (isset($exception_props[$this->proptags["duedate"]])) {
313
				$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
314
			}
315
			if (!empty($props)) {
316
				mapi_setprops($attach, $props);
317
			}
318
319
			mapi_savechanges($attach);
320
		}
321
322
		// Save recurrence data to message
323
		$this->saveRecurrence();
324
325
		return true;
326
	}
327
328
	// Checks to see if the following is true:
329
	// 1) The exception to be created doesn't create two exceptions starting on one day (however, they can END on the same day by modifying duration)
330
	// 2) The exception to be created doesn't 'jump' over another occurrence (which may be an exception itself!)
331
	//
332
	// Both $basedate and $start are in LOCAL time
333
	public function isValidExceptionDate($basedate, $start): bool {
334
		// The way we do this is to look at the days that we're 'moving' the item in the exception. Each
335
		// of these days may only contain the item that we're modifying. Any other item violates the rules.
336
337
		if ($this->isException($basedate)) {
338
			// If we're modifying an exception, we want to look at the days that we're 'moving' compared to where
339
			// the exception used to be.
340
			$oldexception = $this->getChangeException($basedate);
341
			$prevday = $this->dayStartOf($oldexception["start"]);
342
		}
343
		else {
344
			// If its a new exception, we want to look at the original placement of this item.
345
			$prevday = $basedate;
346
		}
347
348
		$startday = $this->dayStartOf($start);
349
350
		// Get all the occurrences on the days between the basedate (may be reversed)
351
		if ($prevday < $startday) {
352
			$items = $this->getItems($this->toGMT($this->tz, $prevday), $this->toGMT($this->tz, $startday + 24 * 60 * 60));
353
		}
354
		else {
355
			$items = $this->getItems($this->toGMT($this->tz, $startday), $this->toGMT($this->tz, $prevday + 24 * 60 * 60));
356
		}
357
358
		// There should now be exactly one item, namely the item that we are modifying. If there are any other items in the range,
359
		// then we abort the change, since one of the rules has been violated.
360
		return count($items) == 1;
361
	}
362
363
	/**
364
	 * Check to see if the exception proposed at a certain basedate is allowed concerning reminder times:.
365
	 *
366
	 * Both must be true:
367
	 * - reminder time of this item is not before the starttime of the previous recurring item
368
	 * - reminder time of the next item is not before the starttime of this item
369
	 *
370
	 * @param date   $basedate        the base date of the exception (LOCAL time of non-exception occurrence)
371
	 * @param string $reminderminutes reminder minutes which is set of the item
372
	 * @param date   $startdate       the startdate of the selected item
373
	 *
374
	 * @returns boolean if the reminder minutes value valid (FALSE if either of the rules above are FALSE)
375
	 */
376
	public function isValidReminderTime($basedate, $reminderminutes, $startdate): bool {
377
		// get all occurrence items before the selected items occurrence starttime
378
		$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], $this->toGMT($this->tz, $basedate));
379
380
		if (!empty($occitems)) {
381
			// as occitems array is sorted in ascending order of startdate, to get the previous occurrence we take the last items in occitems .
382
			$previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]];
383
384
			// if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed
385
			if ($startdate - ($reminderminutes * 60) <= $previousitem_startdate) {
386
				return false;
387
			}
388
		}
389
390
		// Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence)
391
		$currentOcc = $this->getItems($this->toGMT($this->tz, $basedate), 0x7FF00000, 2, true);
392
393
		// If there are another two occurrences, then the first is the current occurrence, and the one after that
394
		// is the next occurrence.
395
		if (count($currentOcc) > 1) {
396
			$next = $currentOcc[1];
397
			// Get reminder time of the next occurrence.
398
			$nextOccReminderTime = $next[$this->proptags["startdate"]] - ($next[$this->proptags["reminder_minutes"]] * 60);
399
			// If the reminder time of the next item is before the start of this item, then that's not allowed
400
			if ($nextOccReminderTime <= $startdate) {
401
				return false;
402
			}
403
		}
404
405
		// All was ok
406
		return true;
407
	}
408
409
	public function setRecurrence($tz, $recur): void {
410
		// only reset timezone if specified
411
		if ($tz) {
412
			$this->tz = $tz;
413
		}
414
415
		$this->recur = $recur;
416
417
		if (!isset($this->recur["changed_occurrences"])) {
418
			$this->recur["changed_occurrences"] = [];
419
		}
420
421
		if (!isset($this->recur["deleted_occurrences"])) {
422
			$this->recur["deleted_occurrences"] = [];
423
		}
424
425
		$this->deleteAttachments();
426
		$this->saveRecurrence();
427
428
		// if client has not set the recurring_pattern then we should generate it and save it
429
		$messageProps = mapi_getprops($this->message, [$this->proptags["recurring_pattern"]]);
0 ignored issues
show
The function mapi_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

429
		$messageProps = /** @scrutinizer ignore-call */ mapi_getprops($this->message, [$this->proptags["recurring_pattern"]]);
Loading history...
430
		if (empty($messageProps[$this->proptags["recurring_pattern"]])) {
431
			$this->saveRecurrencePattern();
432
		}
433
	}
434
435
	// Returns the start or end time of the occurrence on the given base date.
436
	// This assumes that the basedate you supply is in LOCAL time
437
	public function getOccurrenceStart($basedate) {
438
		$daystart = $this->dayStartOf($basedate);
439
440
		return $this->toGMT($this->tz, $daystart + $this->recur["startocc"] * 60);
441
	}
442
443
	public function getOccurrenceEnd($basedate) {
444
		$daystart = $this->dayStartOf($basedate);
445
446
		return $this->toGMT($this->tz, $daystart + $this->recur["endocc"] * 60);
447
	}
448
449
	/**
450
	 * This function returns the next remindertime starting from $timestamp
451
	 * When no next reminder exists, false is returned.
452
	 *
453
	 * Note: Before saving this new reminder time (when snoozing), you must check for
454
	 *       yourself if this reminder time is earlier than your snooze time, else
455
	 *       use your snooze time and not this reminder time.
456
	 *
457
	 * @param mixed $timestamp
458
	 */
459
	public function getNextReminderTime($timestamp) {
460
		/**
461
		 * Get next item from now until forever, but max 1 item with reminder set
462
		 * Note 0x7ff00000 instead of 0x7fffffff because of possible overflow failures when converting to GMT....
463
		 * Here for getting next 10 occurrences assuming that next here we will be able to find
464
		 * nextreminder occurrence in 10 occurrences.
465
		 */
466
		$items = $this->getItems($timestamp, 0x7FF00000, 10, true);
467
468
		// Initially setting nextreminder to false so when no next reminder exists, false is returned.
469
		$nextreminder = false;
470
		/*
471
		 * Loop through all reminder which we get in items variable
472
		 * and check whether the remindertime is greater than timestamp.
473
		 * On the first occurrence of greater nextreminder break the loop
474
		 * and return the value to calling function.
475
		 */
476
		for ($i = 0, $len = count($items); $i < $len; ++$i) {
477
			$item = $items[$i];
478
			$tempnextreminder = $item[$this->proptags["startdate"]] - ($item[$this->proptags["reminder_minutes"]] * 60);
479
480
			// If tempnextreminder is greater than timestamp then save it in nextreminder and break from the loop.
481
			if ($tempnextreminder > $timestamp) {
482
				$nextreminder = $tempnextreminder;
483
				break;
484
			}
485
		}
486
487
		return $nextreminder;
488
	}
489
490
	/**
491
	 * Note: Static function, more like a utility function.
492
	 *
493
	 * Gets all the items (including recurring items) in the specified calendar in the given timeframe. Items are
494
	 * included as a whole if they overlap the interval <$start, $end> (non-inclusive). This means that if the interval
495
	 * is <08:00 - 14:00>, the item [6:00 - 8:00> is NOT included, nor is the item [14:00 - 16:00>. However, the item
496
	 * [7:00 - 9:00> is included as a whole, and is NOT capped to [8:00 - 9:00>.
497
	 *
498
	 * @param $store          resource The store in which the calendar resides
499
	 * @param $calendar       resource The calendar to get the items from
500
	 * @param $viewstart      int Timestamp of beginning of view window
501
	 * @param $viewend        int Timestamp of end of view window
502
	 * @param $propsrequested array Array of properties to return
503
	 *
504
	 * @psalm-param list{0: mixed, 1: mixed, 2?: mixed} $propsrequested
505
	 */
506
	public static function getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested) {
507
		return getCalendarItems($store, $calendar, $viewstart, $viewend, $propsrequested);
508
	}
509
510
	/*
511
	 * CODE BELOW THIS LINE IS FOR INTERNAL USE ONLY
512
	 *****************************************************************************************************************
513
	 */
514
515
	/**
516
	 * Generates and stores recurrence pattern string to recurring_pattern property.
517
	 */
518
	public function saveRecurrencePattern(): void {
519
		// Start formatting the properties in such a way we can apply
520
		// them directly into the recurrence pattern.
521
		$type = $this->recur['type'];
522
		$everyn = $this->recur['everyn'];
523
		$start = $this->recur['start'];
524
		$end = $this->recur['end'];
525
		$term = $this->recur['term'];
526
		$numocc = isset($this->recur['numoccur']) ? $this->recur['numoccur'] : 0;
527
		$startocc = $this->recur['startocc'];
528
		$endocc = $this->recur['endocc'];
529
		$pattern = '';
530
		$occSingleDayRank = false;
531
		$occTimeRange = ($startocc != 0 && $endocc != 0);
532
533
		switch ($type) {
534
			// Daily
535
			case 0x0A:
536
				if ($everyn == 1) {
537
					$type = dgettext('zarafa', 'workday');
538
					$occSingleDayRank = true;
539
				}
540
				elseif ($everyn == (24 * 60)) {
541
					$type = dgettext('zarafa', 'day');
542
					$occSingleDayRank = true;
543
				}
544
				else {
545
					$everyn /= (24 * 60);
546
					$type = dgettext('zarafa', 'days');
547
					$occSingleDayRank = false;
548
				}
549
				break;
550
551
				// Weekly
552
			case 0x0B:
553
				if ($everyn == 1) {
554
					$type = dgettext('zarafa', 'week');
555
					$occSingleDayRank = true;
556
				}
557
				else {
558
					$type = dgettext('zarafa', 'weeks');
559
					$occSingleDayRank = false;
560
				}
561
				break;
562
563
				// Monthly
564
			case 0x0C:
565
				if ($everyn == 1) {
566
					$type = dgettext('zarafa', 'month');
567
					$occSingleDayRank = true;
568
				}
569
				else {
570
					$type = dgettext('zarafa', 'months');
571
					$occSingleDayRank = false;
572
				}
573
				break;
574
575
				// Yearly
576
			case 0x0D:
577
				if ($everyn <= 12) {
578
					$everyn = 1;
579
					$type = dgettext('zarafa', 'year');
580
					$occSingleDayRank = true;
581
				}
582
				else {
583
					$everyn = $everyn / 12;
584
					$type = dgettext('zarafa', 'years');
585
					$occSingleDayRank = false;
586
				}
587
				break;
588
		}
589
590
		// get timings of the first occurrence
591
		$firstoccstartdate = isset($startocc) ? $start + (((int) $startocc) * 60) : $start;
592
		$firstoccenddate = isset($endocc) ? $end + (((int) $endocc) * 60) : $end;
593
594
		$start = gmdate(dgettext('zarafa', 'd-m-Y'), $firstoccstartdate);
595
		$end = gmdate(dgettext('zarafa', 'd-m-Y'), $firstoccenddate);
596
		$startocc = gmdate(dgettext('zarafa', 'G:i'), $firstoccstartdate);
597
		$endocc = gmdate(dgettext('zarafa', 'G:i'), $firstoccenddate);
598
599
		// Based on the properties, we need to generate the recurrence pattern string.
600
		// This is obviously very easy since we can simply concatenate a bunch of strings,
601
		// however this messes up translations for languages which order their words
602
		// differently.
603
		// To improve translation quality we create a series of default strings, in which
604
		// we only have to fill in the correct variables. The base string is thus selected
605
		// based on the available properties.
606
		if ($term == 0x23) {
607
			// Never ends
608
			if ($occTimeRange) {
609
				if ($occSingleDayRank) {
610
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s from %s to %s.'), $type, $start, $startocc, $endocc);
611
				}
612
				else {
613
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s from %s to %s.'), $everyn, $type, $start, $startocc, $endocc);
614
				}
615
			}
616
			else {
617
				if ($occSingleDayRank) {
618
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s.'), $type, $start);
619
				}
620
				else {
621
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s.'), $everyn, $type, $start);
622
				}
623
			}
624
		}
625
		elseif ($term == 0x22) {
626
			// After a number of times
627
			if ($occTimeRange) {
628
				if ($occSingleDayRank) {
629
					$pattern = sprintf(dngettext(
630
						'zarafa',
631
						'Occurs every %s effective %s for %s occurrence from %s to %s.',
632
						'Occurs every %s effective %s for %s occurrences from %s to %s.',
633
						$numocc
634
					), $type, $start, $numocc, $startocc, $endocc);
635
				}
636
				else {
637
					$pattern = sprintf(dngettext(
638
						'zarafa',
639
						'Occurs every %s %s effective %s for %s occurrence from %s to %s.',
640
						'Occurs every %s %s effective %s for %s occurrences %s to %s.',
641
						$numocc
642
					), $everyn, $type, $start, $numocc, $startocc, $endocc);
643
				}
644
			}
645
			else {
646
				if ($occSingleDayRank) {
647
					$pattern = sprintf(dngettext(
648
						'zarafa',
649
						'Occurs every %s effective %s for %s occurrence.',
650
						'Occurs every %s effective %s for %s occurrences.',
651
						$numocc
652
					), $type, $start, $numocc);
653
				}
654
				else {
655
					$pattern = sprintf(dngettext(
656
						'zarafa',
657
						'Occurs every %s %s effective %s for %s occurrence.',
658
						'Occurs every %s %s effective %s for %s occurrences.',
659
						$numocc
660
					), $everyn, $type, $start, $numocc);
661
				}
662
			}
663
		}
664
		elseif ($term == 0x21) {
665
			// After the given enddate
666
			if ($occTimeRange) {
667
				if ($occSingleDayRank) {
668
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s until %s from %s to %s.'), $type, $start, $end, $startocc, $endocc);
669
				}
670
				else {
671
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s until %s from %s to %s.'), $everyn, $type, $start, $end, $startocc, $endocc);
672
				}
673
			}
674
			else {
675
				if ($occSingleDayRank) {
676
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s effective %s until %s.'), $type, $start, $end);
677
				}
678
				else {
679
					$pattern = sprintf(dgettext('zarafa', 'Occurs every %s %s effective %s until %s.'), $everyn, $type, $start, $end);
680
				}
681
			}
682
		}
683
684
		if (!empty($pattern)) {
685
			mapi_setprops($this->message, [$this->proptags["recurring_pattern"] => $pattern]);
0 ignored issues
show
The function mapi_setprops 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

685
			/** @scrutinizer ignore-call */ 
686
   mapi_setprops($this->message, [$this->proptags["recurring_pattern"] => $pattern]);
Loading history...
686
		}
687
	}
688
689
	/*
690
	 * Remove an exception by base_date. This is the base date in local daystart time
691
	 */
692
	/**
693
	 * @param false|int $base_date
694
	 */
695
	public function deleteException($base_date): void {
696
		// Remove all exceptions on $base_date from the deleted and changed occurrences lists
697
698
		// Remove all items in $todelete from deleted_occurrences
699
		$new = [];
700
701
		foreach ($this->recur["deleted_occurrences"] as $entry) {
702
			if ($entry != $base_date) {
703
				$new[] = $entry;
704
			}
705
		}
706
		$this->recur["deleted_occurrences"] = $new;
707
708
		$new = [];
709
710
		foreach ($this->recur["changed_occurrences"] as $entry) {
711
			if (!$this->isSameDay($entry["basedate"], $base_date)) {
712
				$new[] = $entry;
713
			}
714
			else {
715
				$this->deleteExceptionAttachment($this->toGMT($this->tz, $base_date + $this->recur["startocc"] * 60));
716
			}
717
		}
718
719
		$this->recur["changed_occurrences"] = $new;
720
	}
721
722
	/**
723
	 * Function which saves the exception data in an attachment.
724
	 *
725
	 * @param array        $exception_props  the exception data (like any other MAPI appointment)
726
	 * @param array        $exception_recips list of recipients
727
	 * @param mapi_message $copy_attach_from mapi message from which attachments should be copied
728
	 */
729
	public function createExceptionAttachment($exception_props, $exception_recips = [], $copy_attach_from = false): void {
730
		// Create new attachment.
731
		$attachment = mapi_message_createattach($this->message);
0 ignored issues
show
The function mapi_message_createattach 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

731
		$attachment = /** @scrutinizer ignore-call */ mapi_message_createattach($this->message);
Loading history...
732
		$props = [];
733
		$props[PR_ATTACHMENT_FLAGS] = 2;
734
		$props[PR_ATTACHMENT_HIDDEN] = true;
735
		$props[PR_ATTACHMENT_LINKID] = 0;
736
		$props[PR_ATTACH_FLAGS] = 0;
737
		$props[PR_ATTACH_METHOD] = ATTACH_EMBEDDED_MSG;
738
		$props[PR_DISPLAY_NAME] = "Exception";
739
		$props[PR_EXCEPTION_STARTTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]);
740
		$props[PR_EXCEPTION_ENDTIME] = $this->fromGMT($this->tz, $exception_props[$this->proptags["duedate"]]);
741
		mapi_setprops($attachment, $props);
0 ignored issues
show
The function mapi_setprops 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

741
		/** @scrutinizer ignore-call */ 
742
  mapi_setprops($attachment, $props);
Loading history...
742
743
		$imessage = mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
0 ignored issues
show
The function mapi_attach_openobj 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

743
		$imessage = /** @scrutinizer ignore-call */ mapi_attach_openobj($attachment, MAPI_CREATE | MAPI_MODIFY);
Loading history...
744
745
		if ($copy_attach_from) {
746
			$attachmentTable = mapi_message_getattachmenttable($copy_attach_from);
0 ignored issues
show
The function mapi_message_getattachmenttable 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

746
			$attachmentTable = /** @scrutinizer ignore-call */ mapi_message_getattachmenttable($copy_attach_from);
Loading history...
747
			if ($attachmentTable) {
748
				$attachments = mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
0 ignored issues
show
The function mapi_table_queryallrows 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

748
				$attachments = /** @scrutinizer ignore-call */ mapi_table_queryallrows($attachmentTable, [PR_ATTACH_NUM, PR_ATTACH_SIZE, PR_ATTACH_LONG_FILENAME, PR_ATTACHMENT_HIDDEN, PR_DISPLAY_NAME, PR_ATTACH_METHOD]);
Loading history...
749
750
				foreach ($attachments as $attach_props) {
751
					$attach_old = mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
0 ignored issues
show
The function mapi_message_openattach 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

751
					$attach_old = /** @scrutinizer ignore-call */ mapi_message_openattach($copy_attach_from, (int) $attach_props[PR_ATTACH_NUM]);
Loading history...
752
					$attach_newResourceMsg = mapi_message_createattach($imessage);
753
					mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
0 ignored issues
show
The function mapi_copyto 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

753
					/** @scrutinizer ignore-call */ 
754
     mapi_copyto($attach_old, [], [], $attach_newResourceMsg, 0);
Loading history...
754
					mapi_savechanges($attach_newResourceMsg);
0 ignored issues
show
The function mapi_savechanges 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

754
					/** @scrutinizer ignore-call */ 
755
     mapi_savechanges($attach_newResourceMsg);
Loading history...
755
				}
756
			}
757
		}
758
759
		$props = $props + $exception_props;
760
761
		// FIXME: the following piece of code is written to fix the creation
762
		// of an exception. This is only a quickfix as it is not yet possible
763
		// to change an existing exception.
764
		// remove mv properties when needed
765
		foreach ($props as $propTag => $propVal) {
766
			if ((mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)) {
0 ignored issues
show
The function mapi_prop_type 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

766
			if ((/** @scrutinizer ignore-call */ mapi_prop_type($propTag) & MV_FLAG) == MV_FLAG && is_null($propVal)) {
Loading history...
767
				unset($props[$propTag]);
768
			}
769
		}
770
771
		mapi_setprops($imessage, $props);
772
773
		$this->setExceptionRecipients($imessage, $exception_recips, true);
774
775
		mapi_savechanges($imessage);
776
		mapi_savechanges($attachment);
777
	}
778
779
	/**
780
	 * Function which deletes the attachment of an exception.
781
	 *
782
	 * @param mixed $base_date base date of the attachment. Should be in GMT. The attachment
783
	 *                         actually saves the real time of the original date, so we have
784
	 *                         to check whether it's on the same day.
785
	 */
786
	public function deleteExceptionAttachment($base_date): void {
787
		$attachments = mapi_message_getattachmenttable($this->message);
0 ignored issues
show
The function mapi_message_getattachmenttable 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

787
		$attachments = /** @scrutinizer ignore-call */ mapi_message_getattachmenttable($this->message);
Loading history...
788
		// Retrieve only exceptions which are stored as embedded messages
789
		$attach_res = [
790
			RES_PROPERTY,
791
			[
792
				RELOP => RELOP_EQ,
793
				ULPROPTAG => PR_ATTACH_METHOD,
794
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
795
			],
796
		];
797
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
0 ignored issues
show
The function mapi_table_queryallrows 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

797
		$attachRows = /** @scrutinizer ignore-call */ mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
Loading history...
798
799
		foreach ($attachRows as $attachRow) {
800
			$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
0 ignored issues
show
The function mapi_message_openattach 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

800
			$tempattach = /** @scrutinizer ignore-call */ mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
Loading history...
801
			$exception = mapi_attach_openobj($tempattach);
0 ignored issues
show
The function mapi_attach_openobj 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

801
			$exception = /** @scrutinizer ignore-call */ mapi_attach_openobj($tempattach);
Loading history...
802
803
			$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
804
805
			if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
806
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
0 ignored issues
show
The function mapi_message_deleteattach 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

806
				/** @scrutinizer ignore-call */ 
807
    mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
Loading history...
807
			}
808
		}
809
	}
810
811
	/**
812
	 * Function which deletes all attachments of a message.
813
	 */
814
	public function deleteAttachments(): void {
815
		$attachments = mapi_message_getattachmenttable($this->message);
0 ignored issues
show
The function mapi_message_getattachmenttable 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

815
		$attachments = /** @scrutinizer ignore-call */ mapi_message_getattachmenttable($this->message);
Loading history...
816
		$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
0 ignored issues
show
The function mapi_table_queryallrows 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

816
		$attachTable = /** @scrutinizer ignore-call */ mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
Loading history...
817
818
		foreach ($attachTable as $attachRow) {
819
			if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
820
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
0 ignored issues
show
The function mapi_message_deleteattach 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

820
				/** @scrutinizer ignore-call */ 
821
    mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
Loading history...
821
			}
822
		}
823
	}
824
825
	/**
826
	 * Get an exception attachment based on its basedate.
827
	 *
828
	 * @param mixed $base_date
829
	 */
830
	public function getExceptionAttachment($base_date) {
831
		// Retrieve only exceptions which are stored as embedded messages
832
		$attach_res = [
833
			RES_PROPERTY,
834
			[
835
				RELOP => RELOP_EQ,
836
				ULPROPTAG => PR_ATTACH_METHOD,
837
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
838
			],
839
		];
840
		$attachments = mapi_message_getattachmenttable($this->message);
0 ignored issues
show
The function mapi_message_getattachmenttable 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

840
		$attachments = /** @scrutinizer ignore-call */ mapi_message_getattachmenttable($this->message);
Loading history...
841
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
0 ignored issues
show
The function mapi_table_queryallrows 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

841
		$attachRows = /** @scrutinizer ignore-call */ mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
Loading history...
842
843
		if (is_array($attachRows)) {
844
			foreach ($attachRows as $attachRow) {
845
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
0 ignored issues
show
The function mapi_message_openattach 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

845
				$tempattach = /** @scrutinizer ignore-call */ mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
Loading history...
846
				$exception = mapi_attach_openobj($tempattach);
0 ignored issues
show
The function mapi_attach_openobj 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

846
				$exception = /** @scrutinizer ignore-call */ mapi_attach_openobj($tempattach);
Loading history...
847
848
				$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
849
850
				if (!isset($data[$this->proptags["basedate"]])) {
851
					// if no basedate found then it could be embedded message so ignore it
852
					// we need proper restriction to exclude embedded messages as well
853
					continue;
854
				}
855
856
				if ($this->isSameDay($this->fromGMT($this->tz, $data[$this->proptags["basedate"]]), $base_date)) {
857
					return $tempattach;
858
				}
859
			}
860
		}
861
862
		return false;
863
	}
864
865
	/**
866
	 * processOccurrenceItem, adds an item to a list of occurrences, but only if the following criteria are met:
867
	 * - The resulting occurrence (or exception) starts or ends in the interval <$start, $end>
868
	 * - The occurrence isn't specified as a deleted occurrence.
869
	 *
870
	 * @param array $items        reference to the array to be added to
871
	 * @param int   $start        start of timeframe in GMT TIME
872
	 * @param int   $end          end of timeframe in GMT TIME
873
	 * @param int   $basedate     (hour/sec/min assumed to be 00:00:00) in LOCAL TIME OF THE OCCURRENCE
874
	 * @param int   $startocc     start of occurrence since beginning of day in minutes
875
	 * @param int   $endocc       end of occurrence since beginning of day in minutes
876
	 * @param mixed $tz           the timezone info for this occurrence ( applied to $basedate / $startocc / $endocc )
877
	 * @param bool  $reminderonly If TRUE, only add the item if the reminder is set
878
	 *
879
	 * @return null|false
880
	 */
881
	public function processOccurrenceItem(&$items, $start, $end, $basedate, $startocc, $endocc, $tz, $reminderonly) {
882
		$exception = $this->isException($basedate);
883
		if ($exception) {
884
			return false;
885
		}
886
		$occstart = $basedate + $startocc * 60;
887
		$occend = $basedate + $endocc * 60;
888
889
		// Convert to GMT
890
		$occstart = $this->toGMT($tz, $occstart);
891
		$occend = $this->toGMT($tz, $occend);
892
893
		/**
894
		 * FIRST PART: Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
895
		 * see any part of the appointment. Partial overlaps DO match.
896
		 *
897
		 * SECOND PART: check if occurrence is not a zero duration occurrence which
898
		 * starts at 00:00 and ends on 00:00. if it is so, then process
899
		 * the occurrence and send it in response.
900
		 */
901
		if (($occstart >= $end || $occend <= $start) && !($occstart == $occend && $occstart == $start)) {
902
			return;
903
		}
904
905
		// Properties for this occurrence are the same as the main object,
906
		// With these properties overridden
907
		$newitem = $this->messageprops;
908
		$newitem[$this->proptags["startdate"]] = $occstart;
909
		$newitem[$this->proptags["duedate"]] = $occend;
910
		$newitem[$this->proptags["commonstart"]] = $occstart;
911
		$newitem[$this->proptags["commonend"]] = $occend;
912
		$newitem["basedate"] = $basedate;
913
914
		// If reminderonly is set, only add reminders
915
		if ($reminderonly && (!isset($newitem[$this->proptags["reminder"]]) || $newitem[$this->proptags["reminder"]] == false)) {
916
			return;
917
		}
918
919
		$items[] = $newitem;
920
	}
921
922
	/**
923
	 * processExceptionItem, adds an all exception item to a list of occurrences, without any constraint on timeframe.
924
	 *
925
	 * @param array $items reference to the array to be added to
926
	 * @param date  $start start of timeframe in GMT TIME
927
	 * @param date  $end   end of timeframe in GMT TIME
928
	 */
929
	public function processExceptionItems(&$items, $start, $end): void {
930
		$limit = 0;
931
		foreach ($this->recur["changed_occurrences"] as $exception) {
932
			// Convert to GMT
933
			$occstart = $this->toGMT($this->tz, $exception["start"]);
934
			$occend = $this->toGMT($this->tz, $exception["end"]);
935
936
			// Check range criterium. Exact matches (eg when $occstart == $end), do NOT match since you cannot
937
			// see any part of the appointment. Partial overlaps DO match.
938
			if ($occstart >= $end || $occend <= $start) {
939
				continue;
940
			}
941
942
			array_push($items, $this->getExceptionProperties($exception));
943
			if (count($items) == $limit) {
944
				break;
945
			}
946
		}
947
	}
948
949
	/**
950
	 * Function which verifies if on the given date an exception, delete or change, occurs.
951
	 *
952
	 * @param mixed $basedate
953
	 *
954
	 * @return bool true - if an exception occurs on the given date, false - no exception occurs on the given date
955
	 */
956
	public function isException($basedate) {
957
		if ($this->isDeleteException($basedate)) {
958
			return true;
959
		}
960
961
		if ($this->getChangeException($basedate) != false) {
962
			return true;
963
		}
964
965
		return false;
966
	}
967
968
	/**
969
	 * Returns TRUE if there is a DELETE exception on the given base date.
970
	 *
971
	 * @param mixed $basedate
972
	 */
973
	public function isDeleteException($basedate): bool {
974
		// Check if the occurrence is deleted on the specified date
975
		foreach ($this->recur["deleted_occurrences"] as $deleted) {
976
			if ($this->isSameDay($deleted, $basedate)) {
977
				return true;
978
			}
979
		}
980
981
		return false;
982
	}
983
984
	/**
985
	 * Returns the exception if there is a CHANGE exception on the given base date, or FALSE otherwise.
986
	 *
987
	 * @param mixed $basedate
988
	 */
989
	public function getChangeException($basedate) {
990
		// Check if the occurrence is modified on the specified date
991
		foreach ($this->recur["changed_occurrences"] as $changed) {
992
			if ($this->isSameDay($changed["basedate"], $basedate)) {
993
				return $changed;
994
			}
995
		}
996
997
		return false;
998
	}
999
1000
	/**
1001
	 * Function to see if two dates are on the same day.
1002
	 *
1003
	 * @param mixed $date1
1004
	 * @param mixed $date2
1005
	 *
1006
	 * @return bool Returns TRUE when both dates are on the same day
1007
	 */
1008
	public function isSameDay($date1, $date2) {
1009
		$time1 = $this->gmtime($date1);
1010
		$time2 = $this->gmtime($date2);
1011
1012
		return $time1["tm_mon"] == $time2["tm_mon"] && $time1["tm_year"] == $time2["tm_year"] && $time1["tm_mday"] == $time2["tm_mday"];
1013
	}
1014
1015
	/**
1016
	 * Function which sets recipients for an exception.
1017
	 *
1018
	 * The $exception_recips can be provided in 2 ways:
1019
	 *  - A delta which indicates which recipients must be added, removed or deleted.
1020
	 *  - A complete array of the recipients which should be applied to the message.
1021
	 *
1022
	 * The first option is preferred as it will require less work to be executed.
1023
	 *
1024
	 * @param resource $message          exception attachment of recurring item
1025
	 * @param array    $exception_recips list of recipients
1026
	 * @param bool     $copy_orig_recips True to copy all recipients which are on the original
1027
	 *                                   message to the attachment by default. False if only the $exception_recips changes should
1028
	 *                                   be applied.
1029
	 */
1030
	public function setExceptionRecipients($message, $exception_recips, $copy_orig_recips = true): void {
1031
		if (isset($exception_recips['add']) || isset($exception_recips['remove']) || isset($exception_recips['modify'])) {
1032
			$this->setDeltaExceptionRecipients($message, $exception_recips, $copy_orig_recips);
1033
		}
1034
		else {
1035
			$this->setAllExceptionRecipients($message, $exception_recips);
1036
		}
1037
	}
1038
1039
	/**
1040
	 * Function which applies the provided delta for recipients changes to the exception.
1041
	 *
1042
	 * The $exception_recips should be an array containing the following keys:
1043
	 *  - "add": this contains an array of recipients which must be added
1044
	 *  - "remove": This contains an array of recipients which must be removed
1045
	 *  - "modify": This contains an array of recipients which must be modified
1046
	 *
1047
	 * @param mixed $exception
1048
	 * @param array $exception_recips list of recipients
1049
	 * @param bool  $copy_orig_recips True to copy all recipients which are on the original
1050
	 *                                message to the attachment by default. False if only the $exception_recips changes should
1051
	 *                                be applied.
1052
	 */
1053
	public function setDeltaExceptionRecipients($exception, $exception_recips, $copy_orig_recips): void {
1054
		// Check if the recipients from the original message should be copied,
1055
		// if so, open the recipient table of the parent message and apply all
1056
		// rows on the target recipient.
1057
		if ($copy_orig_recips === true) {
1058
			$origTable = mapi_message_getrecipienttable($this->message);
0 ignored issues
show
The function mapi_message_getrecipienttable 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

1058
			$origTable = /** @scrutinizer ignore-call */ mapi_message_getrecipienttable($this->message);
Loading history...
1059
			$recipientRows = mapi_table_queryallrows($origTable, $this->recipprops);
0 ignored issues
show
The function mapi_table_queryallrows 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

1059
			$recipientRows = /** @scrutinizer ignore-call */ mapi_table_queryallrows($origTable, $this->recipprops);
Loading history...
1060
			mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
0 ignored issues
show
The function mapi_message_modifyrecipients 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

1060
			/** @scrutinizer ignore-call */ 
1061
   mapi_message_modifyrecipients($exception, MODRECIP_ADD, $recipientRows);
Loading history...
1061
		}
1062
1063
		// Add organizer to meeting only if it is not organized.
1064
		$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']]);
0 ignored issues
show
The function mapi_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

1064
		$msgprops = /** @scrutinizer ignore-call */ 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']]);
Loading history...
1065
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1066
			$this->addOrganizer($msgprops, $exception_recips['add']);
1067
		}
1068
1069
		// Remove all deleted recipients
1070
		if (isset($exception_recips['remove'])) {
1071
			foreach ($exception_recips['remove'] as &$recip) {
1072
				if (!isset($recip[PR_RECIPIENT_FLAGS]) || $recip[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1073
					$recip[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1074
				}
1075
				else {
1076
					$recip[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1077
				}
1078
				$recip[PR_RECIPIENT_TRACKSTATUS] = olResponseNone;		// No Response required
1079
			}
1080
			unset($recip);
1081
			mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['remove']);
1082
		}
1083
1084
		// Add all new recipients
1085
		if (isset($exception_recips['add'])) {
1086
			mapi_message_modifyrecipients($exception, MODRECIP_ADD, $exception_recips['add']);
1087
		}
1088
1089
		// Modify the existing recipients
1090
		if (isset($exception_recips['modify'])) {
1091
			mapi_message_modifyrecipients($exception, MODRECIP_MODIFY, $exception_recips['modify']);
1092
		}
1093
	}
1094
1095
	/**
1096
	 * Function which applies the provided recipients to the exception, also checks for deleted recipients.
1097
	 *
1098
	 * The $exception_recips should be an array containing all recipients which must be applied
1099
	 * to the exception. This will copy all recipients from the original message and then start filter
1100
	 * out all recipients which are not provided by the $exception_recips list.
1101
	 *
1102
	 * @param resource $message          exception attachment of recurring item
1103
	 * @param array    $exception_recips list of recipients
1104
	 */
1105
	public function setAllExceptionRecipients($message, $exception_recips): void {
1106
		$deletedRecipients = [];
1107
		$useMessageRecipients = false;
1108
1109
		$recipientTable = mapi_message_getrecipienttable($message);
0 ignored issues
show
The function mapi_message_getrecipienttable 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

1109
		$recipientTable = /** @scrutinizer ignore-call */ mapi_message_getrecipienttable($message);
Loading history...
1110
		$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
0 ignored issues
show
The function mapi_table_queryallrows 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

1110
		$recipientRows = /** @scrutinizer ignore-call */ mapi_table_queryallrows($recipientTable, $this->recipprops);
Loading history...
1111
1112
		if (empty($recipientRows)) {
1113
			$useMessageRecipients = true;
1114
			$recipientTable = mapi_message_getrecipienttable($this->message);
1115
			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1116
		}
1117
1118
		// Add organizer to meeting only if it is not organized.
1119
		$msgprops = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']]);
0 ignored issues
show
The function mapi_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

1119
		$msgprops = /** @scrutinizer ignore-call */ 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']]);
Loading history...
1120
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1121
			$this->addOrganizer($msgprops, $exception_recips);
1122
		}
1123
1124
		if (!empty($exception_recips)) {
1125
			foreach ($recipientRows as $recipient) {
1126
				$found = false;
1127
				foreach ($exception_recips as $excep_recip) {
1128
					if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1129
						$found = true;
1130
					}
1131
				}
1132
1133
				if (!$found) {
1134
					$foundInDeletedRecipients = false;
1135
					// Look if the $recipient is in the list of deleted recipients
1136
					if (!empty($deletedRecipients)) {
1137
						foreach ($deletedRecipients as $recip) {
1138
							if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1139
								$foundInDeletedRecipients = true;
1140
								break;
1141
							}
1142
						}
1143
					}
1144
1145
					// If recipient is not in list of deleted recipient, add him
1146
					if (!$foundInDeletedRecipients) {
1147
						if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1148
							$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1149
						}
1150
						else {
1151
							$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1152
						}
1153
						$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;	// No Response required
1154
						$deletedRecipients[] = $recipient;
1155
					}
1156
				}
1157
1158
				// When $message contains a non-empty recipienttable, we must delete the recipients
1159
				// before re-adding them. However, when $message is doesn't contain any recipients,
1160
				// we are using the recipient table of the original message ($this->message)
1161
				// rather then $message. In that case, we don't need to remove the recipients
1162
				// from the $message, as the recipient table is already empty, and
1163
				// mapi_message_modifyrecipients() will throw an error.
1164
				if ($useMessageRecipients === false) {
1165
					mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
0 ignored issues
show
The function mapi_message_modifyrecipients 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

1165
					/** @scrutinizer ignore-call */ 
1166
     mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
Loading history...
1166
				}
1167
			}
1168
			$exception_recips = array_merge($exception_recips, $deletedRecipients);
1169
		}
1170
		else {
1171
			$exception_recips = $recipientRows;
1172
		}
1173
1174
		if (!empty($exception_recips)) {
1175
			// Set the new list of recipients on the exception message, this also removes the existing recipients
1176
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $exception_recips);
1177
		}
1178
	}
1179
1180
	/**
1181
	 * Function returns basedates of all changed occurrences.
1182
	 *
1183
	 * @return array|false array( 0 => 123459321 )
1184
	 *
1185
	 * @psalm-return false|list<mixed>
1186
	 */
1187
	public function getAllExceptions() {
1188
		if (!empty($this->recur["changed_occurrences"])) {
1189
			$result = [];
1190
			foreach ($this->recur["changed_occurrences"] as $exception) {
1191
				$result[] = $exception["basedate"];
1192
			}
1193
1194
			return $result;
1195
		}
1196
1197
		return false;
1198
	}
1199
1200
	/**
1201
	 * Function which adds organizer to recipient list which is passed.
1202
	 * This function also checks if it has organizer.
1203
	 *
1204
	 * @param array $messageProps message properties
1205
	 * @param array $recipients   recipients list of message
1206
	 * @param bool  $isException  true if we are processing recipient of exception
1207
	 */
1208
	public function addOrganizer($messageProps, &$recipients, $isException = false): void {
1209
		$hasOrganizer = false;
1210
		// Check if meeting already has an organizer.
1211
		foreach ($recipients as $key => $recipient) {
1212
			if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
1213
				$hasOrganizer = true;
1214
			}
1215
			elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
1216
				// Recipients for an occurrence
1217
				$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
1218
			}
1219
		}
1220
1221
		if (!$hasOrganizer) {
1222
			// Create organizer.
1223
			$organizer = [];
1224
			$organizer[PR_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
1225
			$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1226
			$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1227
			$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
1228
			$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
1229
			$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
1230
			$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
1231
			$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
1232
			$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
1233
1234
			// Add organizer to recipients list.
1235
			array_unshift($recipients, $organizer);
1236
		}
1237
	}
1238
}
1239
1240
/*
1241
1242
From http://www.ohelp-one.com/new-6765483-3268.html:
1243
1244
Recurrence Data Structure Offset Type Value
1245
1246
0 ULONG (?) Constant : { 0x04, 0x30, 0x04, 0x30}
1247
1248
4 UCHAR 0x0A + recurrence type: 0x0A for daily, 0x0B for weekly, 0x0C for
1249
monthly, 0x0D for yearly
1250
1251
5 UCHAR Constant: { 0x20}
1252
1253
6 ULONG Seems to be a variant of the recurrence type: 1 for daily every n
1254
days, 2 for daily every weekday and weekly, 3 for monthly or yearly. The
1255
special exception is regenerating tasks that regenerate on a weekly basis: 0
1256
is used in that case (I have no idea why).
1257
1258
Here's the recurrence-type-specific data. Because the daily every N days
1259
data are 4 bytes shorter than the data for the other types, the offsets for
1260
the rest of the data will be 4 bytes off depending on the recurrence type.
1261
1262
Daily every N days:
1263
1264
10 ULONG ( N - 1) * ( 24 * 60). I'm not sure what this is used for, but it's consistent.
1265
1266
14 ULONG N * 24 * 60: minutes between recurrences
1267
1268
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1269
regenerating tasks.
1270
1271
Daily every weekday (this is essentially a subtype of weekly recurrence):
1272
1273
10 ULONG 6 * 24 * 60: minutes between recurrences ( a week... sort of)
1274
1275
14 ULONG 1: recur every week (corresponds to the second parameter for weekly
1276
recurrence)
1277
1278
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1279
regenerating tasks.
1280
1281
22 ULONG 0x3E: bitmask for recurring every weekday (corresponds to fourth
1282
parameter for weekly recurrence)
1283
1284
Weekly every N weeks for all events and non-regenerating tasks:
1285
1286
10 ULONG 6 * 24 * 60: minutes between recurrences (a week... sort of)
1287
1288
14 ULONG N: recurrence interval
1289
1290
18 ULONG Constant: 0
1291
1292
22 ULONG Bitmask for determining which days of the week the event recurs on
1293
( 1 << dayOfWeek, where Sunday is 0).
1294
1295
Weekly every N weeks for regenerating tasks: 10 ULONG Constant: 0
1296
1297
14 ULONG N * 7 * 24 * 60: recurrence interval in minutes between occurrences
1298
1299
18 ULONG Constant: 1
1300
1301
Monthly every N months on day D:
1302
1303
10 ULONG This is the most complicated value
1304
in the entire mess. It's basically a very complicated way of stating the
1305
recurrence interval. I tweaked fbs' basic algorithm. DateTime::MonthInDays
1306
simply returns the number of days in a given month, e.g. 31 for July for 28
1307
for February (the algorithm doesn't take into account leap years, but it
1308
doesn't seem to matter). My DateTime object, like Microsoft's COleDateTime,
1309
uses 1-based months (i.e. January is 1, not 0). With that in mind, this
1310
works:
1311
1312
long monthIndex = ( ( ( ( 12 % schedule-=GetInterval()) *
1313
1314
( ( schedule-=GetStartDate().GetYear() - 1601) %
1315
1316
schedule-=GetInterval())) % schedule-=GetInterval()) +
1317
1318
( schedule-=GetStartDate().GetMonth() - 1)) % schedule-=GetInterval();
1319
1320
for( int i = 0; i < monthIndex; i++)
1321
1322
{
1323
1324
value += DateTime::GetDaysInMonth( ( i % 12) + 1) * 24 * 60;
1325
1326
}
1327
1328
This should work for any recurrence interval, including those greater than
1329
12.
1330
1331
14 ULONG N: recurrence interval
1332
1333
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1334
regenerating tasks.
1335
1336
22 ULONG D: day of month the event recurs on (if this value is greater than
1337
the number of days in a given month [e.g. 31 for and recurs in June], then
1338
the event will recur on the last day of the month)
1339
1340
Monthly every N months on the Xth Y (e.g. "2nd Tuesday"):
1341
1342
10 ULONG See above: same as for monthly every N months on day D
1343
1344
14 ULONG N: recurrence interval
1345
1346
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1347
regenerating tasks.
1348
1349
22 ULONG Y: bitmask for determining which day of the week the event recurs
1350
on (see weekly every N weeks). Some useful values are 0x7F for any day, 0x3E
1351
for a weekday, or 0x41 for a weekend day.
1352
1353
26 ULONG X: 1 for first occurrence, 2 for second, etc. 5 for last
1354
occurrence. E.g. for "2nd Tuesday", you should have values of 0x04 for the
1355
prior value and 2 for this one.
1356
1357
Yearly on day D of month M:
1358
1359
10 ULONG M (sort of): This is another messy
1360
value. It's the number of minute since the startning of the year to the
1361
given month. For an explanation of GetDaysInMonth, see monthly every N
1362
months. This will work:
1363
1364
ULONG monthOfYearInMinutes = 0;
1365
1366
for( int i = DateTime::cJanuary; i < schedule-=GetMonth(); i++)
1367
1368
{
1369
1370
monthOfYearInMinutes += DateTime::GetDaysInMonth( i) * 24 * 60;
1371
1372
}
1373
1374
1375
1376
14 ULONG 12: recurrence interval in months. Naturally, 12.
1377
1378
18 ULONG 0 for all events and non-regenerating recurring tasks. 1 for
1379
regenerating tasks.
1380
1381
22 ULONG D: day of month the event recurs on. See monthly every N months on
1382
day D.
1383
1384
Yearly on the Xth Y of month M: 10 ULONG M (sort of): See yearly on day D of
1385
month M.
1386
1387
14 ULONG 12: recurrence interval in months. Naturally, 12.
1388
1389
18 ULONG Constant: 0
1390
1391
22 ULONG Y: see monthly every N months on the Xth Y.
1392
1393
26 ULONG X: see monthly every N months on the Xth Y.
1394
1395
After these recurrence-type-specific values, the offsets will change
1396
depending on the type. For every type except daily every N days, the offsets
1397
will grow by at least 4. For those types using the Xth Y, the offsets will
1398
grow by an additional 4, for a total of 8. The offsets for the rest of these
1399
values will be given for the most basic case, daily every N days, i.e.
1400
without any growth. Adjust as necessary. Also, the presence of exceptions
1401
will change the offsets following the exception data by a variable number of
1402
bytes, so the offsets given in the table are accurate only for those
1403
recurrence patterns without any exceptions.
1404
1405
1406
22 UCHAR Type of pattern termination: 0x21 for terminating on a given date, 0x22 for terminating
1407
after a given number of recurrences, or 0x23 for never terminating
1408
(recurring infinitely)
1409
1410
23 UCHARx3 Constant: { 0x20, 0x00, 0x00}
1411
1412
26 ULONG Number of occurrences in pattern: 0 for infinite recurrence,
1413
otherwise supply the value, even if it terminates on a given date, not after
1414
a given number
1415
1416
30 ULONG Constant: 0
1417
1418
34 ULONG Number of exceptions to pattern (i.e. deleted or changed
1419
occurrences)
1420
1421
.... ULONGxN Base date of each exception, given in hundreds of nanoseconds
1422
since 1601, so see below to turn them into a comprehensible format. The base
1423
date of an exception is the date (and only the date-- not the time) the
1424
exception would have occurred on in the pattern. They must occur in
1425
ascending order.
1426
1427
38 ULONG Number of changed exceptions (i.e. total number of exceptions -
1428
number of deleted exceptions): if there are changed exceptions, again, more
1429
data will be needed, but that will wait
1430
1431
.... ULONGxN Start date (and only the date-- not the time) of each changed
1432
exception, i.e. the exceptions which aren't deleted. These must also occur
1433
in ascending order. If all of the exceptions are deleted, this data will be
1434
absent. If present, they will be in the format above. Any dates that are in
1435
the first list but not in the second are exceptions that have been deleted
1436
(i.e. the difference between the two sets). Note that this is the start date
1437
(including time), not the base date. Given that the values are unordered and
1438
that they can't be matched up against the previous list in this iteration of
1439
the recurrence data (they could in previous ones), it is very difficult to
1440
tell which exceptions are deleted and which are changed. Fortunately, for
1441
this new format, the base dates are given on the attachment representing the
1442
changed exception (described below), so you can simply ignore this list of
1443
changed exceptions. Just create a list of exceptions from the previous list
1444
and assume they're all deleted unless you encounter an attachment with a
1445
matching base date later on.
1446
1447
42 ULONG Start date of pattern given in hundreds of nanoseconds since 1601;
1448
see below for an explanation.
1449
1450
46 ULONG End date of pattern: see start date of pattern
1451
1452
50 ULONG Constant: { 0x06, 0x30, 0x00, 0x00}
1453
1454
NOTE: I find the following 8-byte sequence of bytes to be very useful for
1455
orienting myself when looking at the raw data. If you can find { 0x06, 0x30,
1456
0x00, 0x00, 0x08, 0x30, 0x00, 0x00}, you can use these tables to work either
1457
forwards or backwards to find the data you need. The sequence sort of
1458
delineates certain critical exception-related data and delineates the
1459
exceptions themselves from the rest of the data and is relatively easy to
1460
find. If you're going to be meddling in here a lot, I suggest making a
1461
friend of ol' 0x00003006.
1462
1463
54 UCHAR This number is some kind of version indicator. Use 0x08 for Outlook
1464
2003. I believe 0x06 is Outlook 2000 and possibly 98, while 0x07 is Outlook
1465
XP. This number must be consistent with the features of the data structure
1466
generated by the version of Outlook indicated thereby-- there are subtle
1467
differences between the structures, and, if the version doesn't match the
1468
data, Outlook will sometimes failto read the structure.
1469
1470
55 UCHARx3 Constant: { 0x30, 0x00, 0x00}
1471
1472
58 ULONG Start time of occurrence in minutes: e.g. 0 for midnight or 720 for
1473
12 PM
1474
1475
62 ULONG End time of occurrence in minutes: i.e. start time + duration, e.g.
1476
900 for an event that starts at 12 PM and ends at 3PM
1477
1478
Exception Data 66 USHORT Number of changed exceptions: essentially a check
1479
on the prior occurrence of this value; should be equivalent.
1480
1481
NOTE: The following structure will occur N many times (where N = number of
1482
changed exceptions), and each structure can be of variable length.
1483
1484
.... ULONG Start date of changed exception given in hundreds of nanoseconds
1485
since 1601
1486
1487
.... ULONG End date of changed exception given in hundreds of nanoseconds
1488
since 1601
1489
1490
.... ULONG This is a value I don't clearly understand. It seems to be some
1491
kind of archival value that matches the start time most of the time, but
1492
will lag behind when the start time is changed and then match up again under
1493
certain conditions later. In any case, setting to the same value as the
1494
start time seems to work just fine (more information on this value would be
1495
appreciated).
1496
1497
.... USHORT Bitmask of changes to the exception (see below). This will be 0
1498
if the only changes to the exception were to its start or end time.
1499
1500
.... ULONGxN Numeric values (e.g. label or minutes to remind before the
1501
event) changed in the exception. These will occur in the order of their
1502
corresponding bits (see below). If no numeric values were changed, then
1503
these values will be absent.
1504
1505
NOTE: The following three values constitute a single sub-structure that will
1506
occur N many times, where N is the number of strings that are changed in the
1507
exception. Since there are at most 2 string values that can be excepted
1508
(i.e. subject [or description], and location), there can at most be two of
1509
these, but there may be none.
1510
1511
.... USHORT Length of changed string value with NULL character
1512
1513
.... USHORT Length of changed string value without NULL character (i.e.
1514
previous value - 1)
1515
1516
.... CHARxN Changed string value (without NULL terminator)
1517
1518
Unicode Data NOTE: If a string value was changed on an exception, those
1519
changed string values will reappear here in Unicode format after 8 bytes of
1520
NULL padding (possibly a Unicode terminator?). For each exception with a
1521
changed string value, there will be an identifier, followed by the changed
1522
strings in Unicode. The strings will occur in the order of their
1523
corresponding bits (see below). E.g., if both subject and location were
1524
changed in the exception, there would be the 3-ULONG identifier, then the
1525
length of the subject, then the subject, then the length of the location,
1526
then the location.
1527
1528
70 ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}. This
1529
padding serves as a barrier between the older data structure and the
1530
appended Unicode data. This is the same sequence as the Unicode terminator,
1531
but I'm not sure whether that's its identity or not.
1532
1533
.... ULONGx3 These are the three times used to identify the exception above:
1534
start date, end date, and repeated start date. These should be the same as
1535
they were above.
1536
1537
.... USHORT Length of changed string value without NULL character. This is
1538
given as count of WCHARs, so it should be identical to the value above.
1539
1540
.... WCHARxN Changed string value in Unicode (without NULL terminator)
1541
1542
Terminator ... ULONGxN Constant: { 0x00, 0x00, 0x00, 0x00}. 4 bytes of NULL
1543
padding per changed exception. If there were no changed exceptions, all
1544
you'll need is the final terminator below.
1545
1546
.... ULONGx2 Constant: { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}.
1547
1548
*/
1549