Recurrence   F
last analyzed

Complexity

Total Complexity 187

Size/Duplication

Total Lines 1438
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 639
c 6
b 1
f 0
dl 0
loc 1438
rs 1.961
wmc 187

37 Methods

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

How to fix   Complexity   

Complex Class

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

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

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

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

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

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

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

276
				$this->createExceptionAttachment($exception_props, $exception_recips, /** @scrutinizer ignore-type */ $copy_attach_from);
Loading history...
277
			}
278
		}
279
		else {
280
			$message = mapi_attach_openobj($attach, MAPI_MODIFY);
281
282
			// Set exception properties on embedded message and save
283
			mapi_setprops($message, $exception_props);
284
			$this->setExceptionRecipients($message, $exception_recips, false);
0 ignored issues
show
Bug introduced by
$message of type resource is incompatible with the type resource expected by parameter $message of Recurrence::setExceptionRecipients(). ( Ignorable by Annotation )

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

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

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

359
		$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], /** @scrutinizer ignore-type */ $this->toGMT($this->tz, $basedate));
Loading history...
360
361
		if (!empty($occitems)) {
362
			// as occitems array is sorted in ascending order of startdate, to get the previous occurrence we take the last items in occitems .
363
			$previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]];
364
365
			// if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed
366
			if ($startdate - ($reminderminutes * 60) <= $previousitem_startdate) {
367
				return false;
368
			}
369
		}
370
371
		// Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence)
372
		$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 date; however, parameter $start of BaseRecurrence::getItems() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

987
		$this->setExceptionRecipients(/** @scrutinizer ignore-type */ $imessage, $exception_recips, true);
Loading history...
988
989
		mapi_savechanges($imessage);
990
		mapi_savechanges($attachment);
991
	}
992
993
	/**
994
	 * Function which deletes the attachment of an exception.
995
	 *
996
	 * @param mixed $base_date base date of the attachment. Should be in GMT. The attachment
997
	 *                         actually saves the real time of the original date, so we have
998
	 *                         to check whether it's on the same day.
999
	 */
1000
	public function deleteExceptionAttachment($base_date): void {
1001
		$attachments = mapi_message_getattachmenttable($this->message);
1002
		// Retrieve only exceptions which are stored as embedded messages
1003
		$attach_res = [
1004
			RES_PROPERTY,
1005
			[
1006
				RELOP => RELOP_EQ,
1007
				ULPROPTAG => PR_ATTACH_METHOD,
1008
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
1009
			],
1010
		];
1011
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1012
1013
		foreach ($attachRows as $attachRow) {
1014
			$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1015
			$exception = mapi_attach_openobj($tempattach);
1016
1017
			$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
0 ignored issues
show
Bug introduced by
The function mapi_message_getprops was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1017
			$data = /** @scrutinizer ignore-call */ mapi_message_getprops($exception, [$this->proptags["basedate"]]);
Loading history...
1018
1019
			if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
1020
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1021
			}
1022
		}
1023
	}
1024
1025
	/**
1026
	 * Function which deletes all attachments of a message.
1027
	 */
1028
	public function deleteAttachments(): void {
1029
		$attachments = mapi_message_getattachmenttable($this->message);
1030
		$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
1031
1032
		foreach ($attachTable as $attachRow) {
1033
			if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
1034
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1035
			}
1036
		}
1037
	}
1038
1039
	/**
1040
	 * Get an exception attachment based on its basedate.
1041
	 *
1042
	 * @param mixed $base_date
1043
	 */
1044
	public function getExceptionAttachment($base_date) {
1045
		// Retrieve only exceptions which are stored as embedded messages
1046
		$attach_res = [
1047
			RES_PROPERTY,
1048
			[
1049
				RELOP => RELOP_EQ,
1050
				ULPROPTAG => PR_ATTACH_METHOD,
1051
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
1052
			],
1053
		];
1054
		$attachments = mapi_message_getattachmenttable($this->message);
1055
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1056
1057
		if (is_array($attachRows)) {
1058
			foreach ($attachRows as $attachRow) {
1059
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1060
				$exception = mapi_attach_openobj($tempattach);
1061
1062
				$data = mapi_message_getprops($exception, [$this->proptags["basedate"]]);
0 ignored issues
show
Bug introduced by
The function mapi_message_getprops was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

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

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

1323
		$recipientTable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
1324
		$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1325
1326
		if (empty($recipientRows)) {
1327
			$useMessageRecipients = true;
1328
			$recipientTable = mapi_message_getrecipienttable($this->message);
1329
			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1330
		}
1331
1332
		// Add organizer to meeting only if it is not organized.
1333
		$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
Bug introduced by
$message of type resource is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

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

1333
		$msgprops = mapi_getprops(/** @scrutinizer ignore-type */ $message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_EMAIL_ADDRESS, PR_SENT_REPRESENTING_NAME, PR_SENT_REPRESENTING_ADDRTYPE, PR_SENT_REPRESENTING_SEARCH_KEY, $this->proptags['responsestatus']]);
Loading history...
1334
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1335
			$this->addOrganizer($msgprops, $exception_recips);
1336
		}
1337
1338
		if (!empty($exception_recips)) {
1339
			foreach ($recipientRows as $recipient) {
1340
				$found = false;
1341
				foreach ($exception_recips as $excep_recip) {
1342
					if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1343
						$found = true;
1344
					}
1345
				}
1346
1347
				if (!$found) {
1348
					$foundInDeletedRecipients = false;
1349
					// Look if the $recipient is in the list of deleted recipients
1350
					if (!empty($deletedRecipients)) {
1351
						foreach ($deletedRecipients as $recip) {
1352
							if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1353
								$foundInDeletedRecipients = true;
1354
								break;
1355
							}
1356
						}
1357
					}
1358
1359
					// If recipient is not in list of deleted recipient, add him
1360
					if (!$foundInDeletedRecipients) {
1361
						if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1362
							$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1363
						}
1364
						else {
1365
							$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1366
						}
1367
						$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;	// No Response required
1368
						$deletedRecipients[] = $recipient;
1369
					}
1370
				}
1371
1372
				// When $message contains a non-empty recipienttable, we must delete the recipients
1373
				// before re-adding them. However, when $message is doesn't contain any recipients,
1374
				// we are using the recipient table of the original message ($this->message)
1375
				// rather then $message. In that case, we don't need to remove the recipients
1376
				// from the $message, as the recipient table is already empty, and
1377
				// mapi_message_modifyrecipients() will throw an error.
1378
				if ($useMessageRecipients === false) {
1379
					mapi_message_modifyrecipients($message, MODRECIP_REMOVE, [$recipient]);
0 ignored issues
show
Bug introduced by
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_modifyrecipients(). ( Ignorable by Annotation )

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

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