Recurrence   F
last analyzed

Complexity

Total Complexity 191

Size/Duplication

Total Lines 1458
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 646
c 3
b 1
f 0
dl 0
loc 1458
rs 1.954
wmc 191

37 Methods

Rating   Name   Duplication   Size   Complexity  
A getI18nRecTermNrOcc() 0 65 4
A getExceptionAttachment() 0 33 5
A isException() 0 10 3
A isSameDay() 0 5 3
A getChangeException() 0 9 3
A getI18nRecTermDate() 0 45 4
B setDeltaExceptionRecipients() 0 39 10
B saveRecurrencePattern() 0 77 6
A getOccDate() 0 8 4
A deleteExceptionAttachment() 0 21 3
A getI18nRecTermNoEnd() 0 40 4
A setExceptionRecipients() 0 6 4
A getAllExceptions() 0 11 3
A isDeleteException() 0 9 3
A processExceptionItems() 0 16 5
A deleteAttachments() 0 7 4
A getI18nTime() 0 2 1
D setAllExceptionRecipients() 0 72 19
B createExceptionAttachment() 0 48 7
B processOccurrenceItem() 0 39 9
A deleteException() 0 25 5
B addOrganizer() 0 28 8
A isValidReminderTime() 0 31 5
A getI18RecTypeDaily() 0 23 3
A getCalendarItems() 0 2 1
F createException() 0 97 18
A getI18RecTypeYearly() 0 16 2
A getI18RecTypeWeekly() 0 26 5
A isValidExceptionDate() 0 28 3
F modifyException() 0 107 22
A __construct() 0 49 2
A getI18nRecurrenceType() 0 18 1
A getNextReminderTime() 0 29 3
A getOccurrenceStart() 0 4 1
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-2024 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
97
			$this->proptags = getPropIdsFromStrings($store, $properties);
98
		}
99
100
		parent::__construct($store, $message);
101
	}
102
103
	/**
104
	 * Create an exception.
105
	 *
106
	 * @param array $exception_props  the exception properties (same properties as normal recurring items)
107
	 * @param mixed $base_date        the base date of the exception (LOCAL time of non-exception occurrence)
108
	 * @param bool  $delete           true - delete occurrence, false - create new exception or modify existing
109
	 * @param array $exception_recips list of recipients
110
	 * @param mixed $copy_attach_from mapi message from which attachments should be copied
111
	 */
112
	public function createException($exception_props, $base_date, $delete = false, $exception_recips = [], $copy_attach_from = false): bool {
113
		$baseday = $this->dayStartOf($base_date);
114
		$basetime = $baseday + $this->recur["startocc"] * 60;
115
116
		// Remove any pre-existing exception on this base date
117
		if ($this->isException($baseday)) {
118
			$this->deleteException($baseday); // note that deleting an exception is different from creating a deleted exception (deleting an occurrence).
119
		}
120
121
		if (!$delete) {
122
			if (isset($exception_props[$this->proptags["startdate"]]) && !$this->isValidExceptionDate($base_date, $this->fromGMT($this->tz, $exception_props[$this->proptags["startdate"]]))) {
123
				return false;
124
			}
125
			// Properties in the attachment are the properties of the base object, plus $exception_props plus the base date
126
			foreach (["subject", "location", "label", "reminder", "reminder_minutes", "alldayevent", "busystatus"] as $propname) {
127
				if (isset($this->messageprops[$this->proptags[$propname]])) {
128
					$props[$this->proptags[$propname]] = $this->messageprops[$this->proptags[$propname]];
129
				}
130
			}
131
132
			$props[PR_MESSAGE_CLASS] = "IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}";
133
			unset($exception_props[PR_MESSAGE_CLASS], $exception_props[PR_ICON_INDEX]);
134
135
			$props = $exception_props + $props;
136
137
			// Basedate in the exception attachment is the GMT time at which the original occurrence would have been
138
			$props[$this->proptags["basedate"]] = $this->toGMT($this->tz, $basetime);
139
140
			if (!isset($exception_props[$this->proptags["startdate"]])) {
141
				$props[$this->proptags["startdate"]] = $this->getOccurrenceStart($base_date);
142
			}
143
144
			if (!isset($exception_props[$this->proptags["duedate"]])) {
145
				$props[$this->proptags["duedate"]] = $this->getOccurrenceEnd($base_date);
146
			}
147
148
			// synchronize commonstart/commonend with startdate/duedate
149
			if (isset($props[$this->proptags["startdate"]])) {
150
				$props[$this->proptags["commonstart"]] = $props[$this->proptags["startdate"]];
151
			}
152
153
			if (isset($props[$this->proptags["duedate"]])) {
154
				$props[$this->proptags["commonend"]] = $props[$this->proptags["duedate"]];
155
			}
156
157
			// Save the data into an attachment
158
			$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

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

379
		$occitems = $this->getItems($this->messageprops[$this->proptags["startdate"]], /** @scrutinizer ignore-type */ $this->toGMT($this->tz, $basedate));
Loading history...
380
381
		if (!empty($occitems)) {
382
			// as occitems array is sorted in ascending order of startdate, to get the previous occurrence we take the last items in occitems .
383
			$previousitem_startdate = $occitems[count($occitems) - 1][$this->proptags["startdate"]];
384
385
			// if our reminder is set before or equal to the beginning of the previous occurrence, then that's not allowed
386
			if ($startdate - ($reminderminutes * 60) <= $previousitem_startdate) {
387
				return false;
388
			}
389
		}
390
391
		// Get the endtime of the current occurrence and find the next two occurrences (including the current occurrence)
392
		$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

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

1007
		$this->setExceptionRecipients(/** @scrutinizer ignore-type */ $imessage, $exception_recips, true);
Loading history...
1008
1009
		mapi_savechanges($imessage);
1010
		mapi_savechanges($attachment);
1011
	}
1012
1013
	/**
1014
	 * Function which deletes the attachment of an exception.
1015
	 *
1016
	 * @param mixed $base_date base date of the attachment. Should be in GMT. The attachment
1017
	 *                         actually saves the real time of the original date, so we have
1018
	 *                         to check whether it's on the same day.
1019
	 */
1020
	public function deleteExceptionAttachment($base_date): void {
1021
		$attachments = mapi_message_getattachmenttable($this->message);
1022
		// Retrieve only exceptions which are stored as embedded messages
1023
		$attach_res = [
1024
			RES_PROPERTY,
1025
			[
1026
				RELOP => RELOP_EQ,
1027
				ULPROPTAG => PR_ATTACH_METHOD,
1028
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
1029
			],
1030
		];
1031
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1032
1033
		foreach ($attachRows as $attachRow) {
1034
			$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1035
			$exception = mapi_attach_openobj($tempattach);
1036
1037
			$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

1037
			$data = /** @scrutinizer ignore-call */ mapi_message_getprops($exception, [$this->proptags["basedate"]]);
Loading history...
1038
1039
			if ($this->dayStartOf($this->fromGMT($this->tz, $data[$this->proptags["basedate"]])) == $this->dayStartOf($base_date)) {
1040
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1041
			}
1042
		}
1043
	}
1044
1045
	/**
1046
	 * Function which deletes all attachments of a message.
1047
	 */
1048
	public function deleteAttachments(): void {
1049
		$attachments = mapi_message_getattachmenttable($this->message);
1050
		$attachTable = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM, PR_ATTACHMENT_HIDDEN]);
1051
1052
		foreach ($attachTable as $attachRow) {
1053
			if (isset($attachRow[PR_ATTACHMENT_HIDDEN]) && $attachRow[PR_ATTACHMENT_HIDDEN]) {
1054
				mapi_message_deleteattach($this->message, $attachRow[PR_ATTACH_NUM]);
1055
			}
1056
		}
1057
	}
1058
1059
	/**
1060
	 * Get an exception attachment based on its basedate.
1061
	 *
1062
	 * @param mixed $base_date
1063
	 */
1064
	public function getExceptionAttachment($base_date) {
1065
		// Retrieve only exceptions which are stored as embedded messages
1066
		$attach_res = [
1067
			RES_PROPERTY,
1068
			[
1069
				RELOP => RELOP_EQ,
1070
				ULPROPTAG => PR_ATTACH_METHOD,
1071
				VALUE => [PR_ATTACH_METHOD => ATTACH_EMBEDDED_MSG],
1072
			],
1073
		];
1074
		$attachments = mapi_message_getattachmenttable($this->message);
1075
		$attachRows = mapi_table_queryallrows($attachments, [PR_ATTACH_NUM], $attach_res);
1076
1077
		if (is_array($attachRows)) {
1078
			foreach ($attachRows as $attachRow) {
1079
				$tempattach = mapi_message_openattach($this->message, $attachRow[PR_ATTACH_NUM]);
1080
				$exception = mapi_attach_openobj($tempattach);
1081
1082
				$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

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

1343
		$recipientTable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
1344
		$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1345
1346
		if (empty($recipientRows)) {
1347
			$useMessageRecipients = true;
1348
			$recipientTable = mapi_message_getrecipienttable($this->message);
1349
			$recipientRows = mapi_table_queryallrows($recipientTable, $this->recipprops);
1350
		}
1351
1352
		// Add organizer to meeting only if it is not organized.
1353
		$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

1353
		$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...
1354
		if (isset($msgprops[$this->proptags['responsestatus']]) && $msgprops[$this->proptags['responsestatus']] != olResponseOrganized) {
1355
			$this->addOrganizer($msgprops, $exception_recips);
1356
		}
1357
1358
		if (!empty($exception_recips)) {
1359
			foreach ($recipientRows as $recipient) {
1360
				$found = false;
1361
				foreach ($exception_recips as $excep_recip) {
1362
					if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recipient[PR_SEARCH_KEY] == $excep_recip[PR_SEARCH_KEY]) {
1363
						$found = true;
1364
					}
1365
				}
1366
1367
				if (!$found) {
1368
					$foundInDeletedRecipients = false;
1369
					// Look if the $recipient is in the list of deleted recipients
1370
					if (!empty($deletedRecipients)) {
1371
						foreach ($deletedRecipients as $recip) {
1372
							if (isset($recipient[PR_SEARCH_KEY], $excep_recip[PR_SEARCH_KEY]) && $recip[PR_SEARCH_KEY] == $recipient[PR_SEARCH_KEY]) {
1373
								$foundInDeletedRecipients = true;
1374
								break;
1375
							}
1376
						}
1377
					}
1378
1379
					// If recipient is not in list of deleted recipient, add him
1380
					if (!$foundInDeletedRecipients) {
1381
						if (!isset($recipient[PR_RECIPIENT_FLAGS]) || $recipient[PR_RECIPIENT_FLAGS] != (recipReserved | recipExceptionalDeleted | recipSendable)) {
1382
							$recipient[PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalDeleted;
1383
						}
1384
						else {
1385
							$recipient[PR_RECIPIENT_FLAGS] = recipReserved | recipExceptionalDeleted | recipSendable;
1386
						}
1387
						$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;	// No Response required
1388
						$deletedRecipients[] = $recipient;
1389
					}
1390
				}
1391
1392
				// When $message contains a non-empty recipienttable, we must delete the recipients
1393
				// before re-adding them. However, when $message is doesn't contain any recipients,
1394
				// we are using the recipient table of the original message ($this->message)
1395
				// rather then $message. In that case, we don't need to remove the recipients
1396
				// from the $message, as the recipient table is already empty, and
1397
				// mapi_message_modifyrecipients() will throw an error.
1398
				if ($useMessageRecipients === false) {
1399
					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

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