source: trunk/zpush/backend/zarafa/mapi/class.baserecurrence.php @ 7589

Revision 7589, 86.2 KB checked in by douglas, 11 years ago (diff)

Ticket #3209 - Integrar módulo de sincronização Z-push ao Expresso

Line 
1<?php
2/*
3 * Copyright 2005 - 2012  Zarafa B.V.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License, version 3,
7 * as published by the Free Software Foundation with the following additional
8 * term according to sec. 7:
9 *
10 * According to sec. 7 of the GNU Affero General Public License, version
11 * 3, the terms of the AGPL are supplemented with the following terms:
12 *
13 * "Zarafa" is a registered trademark of Zarafa B.V. The licensing of
14 * the Program under the AGPL does not imply a trademark license.
15 * Therefore any rights, title and interest in our trademarks remain
16 * entirely with us.
17 *
18 * However, if you propagate an unmodified version of the Program you are
19 * allowed to use the term "Zarafa" to indicate that you distribute the
20 * Program. Furthermore you may use our trademarks where it is necessary
21 * to indicate the intended purpose of a product or service provided you
22 * use it in accordance with honest practices in industrial or commercial
23 * matters.  If you want to propagate modified versions of the Program
24 * under the name "Zarafa" or "Zarafa Server", you may only do so if you
25 * have a written permission by Zarafa B.V. (to acquire a permission
26 * please contact Zarafa at trademark@zarafa.com).
27 *
28 * The interactive user interface of the software displays an attribution
29 * notice containing the term "Zarafa" and/or the logo of Zarafa.
30 * Interactive user interfaces of unmodified and modified versions must
31 * display Appropriate Legal Notices according to sec. 5 of the GNU
32 * Affero General Public License, version 3, when you propagate
33 * unmodified or modified versions of the Program. In accordance with
34 * sec. 7 b) of the GNU Affero General Public License, version 3, these
35 * Appropriate Legal Notices must retain the logo of Zarafa or display
36 * the words "Initial Development by Zarafa" if the display of the logo
37 * is not reasonably feasible for technical reasons. The use of the logo
38 * of Zarafa in Legal Notices is allowed for unmodified and modified
39 * versions of the software.
40 *
41 * This program is distributed in the hope that it will be useful,
42 * but WITHOUT ANY WARRANTY; without even the implied warranty of
43 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
44 * GNU Affero General Public License for more details.
45 *
46 * You should have received a copy of the GNU Affero General Public License
47 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
48 *
49 */
50
51    /**
52     * BaseRecurrence
53     * this class is superclass for recurrence for appointments and tasks. This class provides all
54     * basic features of recurrence.
55     */
56    class BaseRecurrence
57    {
58        /**
59         * @var object Mapi Message Store (may be null if readonly)
60         */
61        var $store;
62
63        /**
64         * @var object Mapi Message (may be null if readonly)
65         */
66        var $message;
67
68        /**
69         * @var array Message Properties
70         */
71        var $messageprops;
72
73        /**
74         * @var array list of property tags
75         */
76        var $proptags;
77
78        /**
79         * @var recurrence data of this calendar item
80         */
81        var $recur;
82
83        /**
84         * @var Timezone data of this calendar item
85         */
86        var $tz;
87
88        /**
89         * Constructor
90         * @param resource $store MAPI Message Store Object
91         * @param resource $message the MAPI (appointment) message
92         * @param array $properties the list of MAPI properties the message has.
93         */
94        function BaseRecurrence($store, $message)
95        {
96            $this->store = $store;
97
98            if(is_array($message)) {
99                $this->messageprops = $message;
100            } else {
101                $this->message = $message;
102                $this->messageprops = mapi_getprops($this->message, $this->proptags);
103            }
104
105            if(isset($this->messageprops[$this->proptags["recurring_data"]])) {
106                // There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface
107                if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255) {
108                    $this->getFullRecurrenceBlob();
109                }
110
111                $this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]);
112            }
113            if(isset($this->proptags["timezone_data"]) && isset($this->messageprops[$this->proptags["timezone_data"]])) {
114                $this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]);
115            }
116        }
117
118        function getRecurrence()
119        {
120            return $this->recur;
121        }
122
123        function getFullRecurrenceBlob()
124        {
125            $message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]);
126
127            $recurrBlob = '';
128            $stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
129            $stat = mapi_stream_stat($stream);
130
131            for ($i = 0; $i < $stat['cb']; $i += 1024) {
132                $recurrBlob .= mapi_stream_read($stream, 1024);
133            }
134
135            if (!empty($recurrBlob)) {
136                $this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob;
137            }
138        }
139
140        /**
141        * Function for parsing the Recurrence value of a Calendar item.
142        *
143        * Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the
144        * data to this function
145        *
146        * Returns a structure containing the data:
147        *
148        * type        - type of recurrence: day=10, week=11, month=12, year=13
149        * subtype    - type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday)
150        * start    - unix timestamp of first occurrence
151        * end        - unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1
152        * numoccur     - occurrences (may be very large when there is no end data)
153        *
154        * then, for each type:
155        *
156        * Daily:
157        *  everyn    - every [everyn] days in minutes
158        *  regen    - regenerating event (like tasks)
159        *
160        * Weekly:
161        *  everyn    - every [everyn] weeks in weeks
162        *  regen    - regenerating event (like tasks)
163        *  weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
164        *
165        * Monthly:
166        *  everyn    - every [everyn] months
167        *  regen    - regenerating event (like tasks)
168        *
169        *  subtype 2:
170        *      monthday - on day [monthday] of the month
171        *
172        *  subtype 3:
173        *      weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
174        *   nday    - on [nday]'th [weekdays] of the month
175        *
176        * Yearly:
177        *  everyn    - every [everyn] months (12, 24, 36, ...)
178        *  month    - in month [month] (although the month is encoded in minutes since the startning of the year ........)
179        *  regen    - regenerating event (like tasks)
180        *
181        *  subtype 2:
182        *   monthday - on day [monthday] of the month
183        *
184        *  subtype 3:
185        *   weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
186        *      nday    - on [nday]'th [weekdays] of the month [month]
187        * @param string $rdata Binary string
188        * @return array recurrence data.
189        */
190        function parseRecurrence($rdata)
191        {
192            if (strlen($rdata) < 10) {
193                return;
194            }
195
196            $ret["changed_occurences"] = array();
197            $ret["deleted_occurences"] = array();
198
199            $data = unpack("Vconst1/Crtype/Cconst2/Vrtype2", $rdata);
200
201            $ret["type"] = $data["rtype"];
202            $ret["subtype"] = $data["rtype2"];
203            $rdata = substr($rdata, 10);
204
205            switch ($data["rtype"])
206            {
207                case 0x0a:
208                    // Daily
209                    if (strlen($rdata) < 12) {
210                        return $ret;
211                    }
212
213                    $data = unpack("Vunknown/Veveryn/Vregen", $rdata);
214                    $ret["everyn"] = $data["everyn"];
215                    $ret["regen"] = $data["regen"];
216
217                    switch($ret["subtype"])
218                    {
219                        case 0:
220                            $rdata = substr($rdata, 12);
221                            break;
222                        case 1:
223                            $rdata = substr($rdata, 16);
224                            break;
225                    }
226
227                    break;
228
229                case 0x0b:
230                    // Weekly
231                    if (strlen($rdata) < 16) {
232                        return $ret;
233                    }
234
235                    $data = unpack("Vconst1/Veveryn/Vregen", $rdata);
236                    $rdata = substr($rdata, 12);
237
238                    $ret["everyn"] = $data["everyn"];
239                    $ret["regen"] = $data["regen"];
240                    $ret["weekdays"] = 0;
241
242                    if ($data["regen"] == 0) {
243                        $data = unpack("Vweekdays", $rdata);
244                        $rdata = substr($rdata, 4);
245
246                        $ret["weekdays"] = $data["weekdays"];
247                    }
248                    break;
249
250                case 0x0c:
251                    // Monthly
252                    if (strlen($rdata) < 16) {
253                        return $ret;
254                    }
255
256                    $data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata);
257
258                    $ret["everyn"] = $data["everyn"];
259                    $ret["regen"] = $data["regen"];
260
261                    if ($ret["subtype"] == 3) {
262                        $ret["weekdays"] = $data["monthday"];
263                    } else {
264                        $ret["monthday"] = $data["monthday"];
265                    }
266
267                    $rdata = substr($rdata, 16);
268
269                    if ($ret["subtype"] == 3) {
270                        $data = unpack("Vnday", $rdata);
271                        $ret["nday"] = $data["nday"];
272                        $rdata = substr($rdata, 4);
273                    }
274                    break;
275
276                case 0x0d:
277                    // Yearly
278                    if (strlen($rdata) < 16)
279                        return $ret;
280
281                    $data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata);
282
283                    $ret["month"] = $data["month"];
284                    $ret["everyn"] = $data["everyn"];
285                    $ret["regen"] = $data["regen"];
286
287                    if ($ret["subtype"] == 3) {
288                        $ret["weekdays"] = $data["monthday"];
289                    } else {
290                        $ret["monthday"] = $data["monthday"];
291                    }
292
293                    $rdata = substr($rdata, 16);
294
295                    if ($ret["subtype"] == 3) {
296                        $data = unpack("Vnday", $rdata);
297                        $ret["nday"] = $data["nday"];
298                        $rdata = substr($rdata, 4);
299                    }
300                    break;
301            }
302
303            if (strlen($rdata) < 16) {
304                return $ret;
305            }
306
307            $data = unpack("Cterm/C3const1/Vnumoccur/Vconst2/Vnumexcept", $rdata);
308
309            $rdata = substr($rdata, 16);
310
311            $ret["term"] = $data["term"];
312            $ret["numoccur"] = $data["numoccur"];
313            $ret["numexcept"] = $data["numexcept"];
314
315            // exc_base_dates are *all* the base dates that have been either deleted or modified
316            $exc_base_dates = array();
317            for($i = 0; $i < $ret["numexcept"]; $i++)
318            {
319                if (strlen($rdata) < 4) {
320                    // We shouldn't arrive here, because that implies
321                    // numexcept does not match the amount of data
322                    // which is available for the exceptions.
323                    return $ret;
324                }
325                $data = unpack("Vbasedate", $rdata);
326                $rdata = substr($rdata, 4);
327                $exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]);
328            }
329
330            if (strlen($rdata) < 4) {
331                return $ret;
332            }
333
334            $data = unpack("Vnumexceptmod", $rdata);
335            $rdata = substr($rdata, 4);
336
337            $ret["numexceptmod"] = $data["numexceptmod"];
338
339            // exc_changed are the base dates of *modified* occurrences. exactly what is modified
340            // is in the attachments *and* in the data further down this function.
341            $exc_changed = array();
342            for($i = 0; $i < $ret["numexceptmod"]; $i++)
343            {
344                if (strlen($rdata) < 4) {
345                    // We shouldn't arrive here, because that implies
346                    // numexceptmod does not match the amount of data
347                    // which is available for the exceptions.
348                    return $ret;
349                }
350                $data = unpack("Vstartdate", $rdata);
351                $rdata = substr($rdata, 4);
352                $exc_changed[] = $this->recurDataToUnixData($data["startdate"]);
353            }
354
355            if (strlen($rdata) < 8) {
356                return $ret;
357            }
358
359            $data = unpack("Vstart/Vend", $rdata);
360            $rdata = substr($rdata, 8);
361
362            $ret["start"] = $this->recurDataToUnixData($data["start"]);
363            $ret["end"] = $this->recurDataToUnixData($data["end"]);
364
365            // this is where task recurrence stop
366            if (strlen($rdata) < 16) {
367                return $ret;
368            }
369
370            $data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata);
371            $rdata = substr($rdata, 16);
372
373            $ret["startocc"] = $data["startmin"];
374            $ret["endocc"] = $data["endmin"];
375            $readerversion = $data["readerversion"];
376            $writerversion = $data["writerversion"];
377
378            $data = unpack("vnumber", $rdata);
379            $rdata = substr($rdata, 2);
380
381            $nexceptions = $data["number"];
382            $exc_changed_details = array();
383
384            // Parse n modified exceptions
385            for($i=0;$i<$nexceptions;$i++)
386            {
387                $item = array();
388
389                // Get exception startdate, enddate and basedate (the date at which the occurrence would have started)
390                $data = unpack("Vstartdate/Venddate/Vbasedate", $rdata);
391                $rdata = substr($rdata, 12);
392
393                // Convert recurtimestamp to unix timestamp
394                $startdate = $this->recurDataToUnixData($data["startdate"]);
395                $enddate = $this->recurDataToUnixData($data["enddate"]);
396                $basedate = $this->recurDataToUnixData($data["basedate"]);
397
398                // Set the right properties
399                $item["basedate"] = $this->dayStartOf($basedate);
400                $item["start"] = $startdate;
401                $item["end"] = $enddate;
402
403                $data = unpack("vbitmask", $rdata);
404                $rdata = substr($rdata, 2);
405                $item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions
406
407                // Bitmask to verify what properties are changed
408                $bitmask = $data["bitmask"];
409
410                // ARO_SUBJECT: 0x0001
411                // Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject
412                if(($bitmask &(1 << 0))) {
413                    $data = unpack("vnull_length/vlength", $rdata);
414                    $rdata = substr($rdata, 4);
415
416                    $length = $data["length"];
417                    $item["subject"] = ""; // Normalized subject
418                    for($j = 0; $j < $length && strlen($rdata); $j++)
419                    {
420                        $data = unpack("Cchar", $rdata);
421                        $rdata = substr($rdata, 1);
422
423                        $item["subject"] .= chr($data["char"]);
424                    }
425                }
426
427                // ARO_MEETINGTYPE: 0x0002
428                if(($bitmask &(1 << 1))) {
429                    $rdata = substr($rdata, 4);
430                    // Attendees modified: no data here (only in attachment)
431                }
432
433                // ARO_REMINDERDELTA: 0x0004
434                // Look for field: ReminderDelta (4b)
435                if(($bitmask &(1 << 2))) {
436                    $data = unpack("Vremind_before", $rdata);
437                    $rdata = substr($rdata, 4);
438
439                    $item["remind_before"] = $data["remind_before"];
440                }
441
442                // ARO_REMINDER: 0x0008
443                // Look field: ReminderSet (4b)
444                if(($bitmask &(1 << 3))) {
445                    $data = unpack("Vreminder_set", $rdata);
446                    $rdata = substr($rdata, 4);
447
448                    $item["reminder_set"] = $data["reminder_set"];
449                }
450
451                // ARO_LOCATION: 0x0010
452                // Look for fields: LocationLength (2b), LocationLength2 (2b) and Location
453                // Similar to ARO_SUBJECT above.
454                if(($bitmask &(1 << 4))) {
455                    $data = unpack("vnull_length/vlength", $rdata);
456                    $rdata = substr($rdata, 4);
457
458                    $item["location"] = "";
459
460                    $length = $data["length"];
461                    $data = substr($rdata, 0, $length);
462                    $rdata = substr($rdata, $length);
463
464                    $item["location"] .= $data;
465                }
466
467                // ARO_BUSYSTATUS: 0x0020
468                // Look for field: BusyStatus (4b)
469                if(($bitmask &(1 << 5))) {
470                    $data = unpack("Vbusystatus", $rdata);
471                    $rdata = substr($rdata, 4);
472
473                    $item["busystatus"] = $data["busystatus"];
474                }
475
476                // ARO_ATTACHMENT: 0x0040
477                if(($bitmask &(1 << 6))) {
478                    // no data: RESERVED
479                    $rdata = substr($rdata, 4);
480                }
481
482                // ARO_SUBTYPE: 0x0080
483                // Look for field: SubType (4b). Determines whether it is an allday event.
484                if(($bitmask &(1 << 7))) {
485                    $data = unpack("Vallday", $rdata);
486                    $rdata = substr($rdata, 4);
487
488                    $item["alldayevent"] = $data["allday"];
489                }
490
491                // ARO_APPTCOLOR: 0x0100
492                // Look for field: AppointmentColor (4b)
493                if(($bitmask &(1 << 8))) {
494                    $data = unpack("Vlabel", $rdata);
495                    $rdata = substr($rdata, 4);
496
497                    $item["label"] = $data["label"];
498                }
499
500                // ARO_EXCEPTIONAL_BODY: 0x0200
501                if(($bitmask &(1 << 9))) {
502                    // Notes or Attachments modified: no data here (only in attachment)
503                }
504
505                array_push($exc_changed_details, $item);
506            }
507
508            /**
509             * We now have $exc_changed, $exc_base_dates and $exc_changed_details
510             * We will ignore $exc_changed, as this information is available in $exc_changed_details
511             * also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item
512             * has been deleted.
513             */
514
515            // Find deleted occurrences
516            $deleted_occurences = array();
517
518            foreach($exc_base_dates as $base_date) {
519                $found = false;
520
521                foreach($exc_changed_details as $details) {
522                    if($details["basedate"] == $base_date) {
523                        $found = true;
524                        break;
525                    }
526                }
527                if(! $found) {
528                    // item was not in exc_changed_details, so it must be deleted
529                    $deleted_occurences[] = $base_date;
530                }
531            }
532
533            $ret["deleted_occurences"] = $deleted_occurences;
534            $ret["changed_occurences"] = $exc_changed_details;
535
536            // enough data for normal exception (no extended data)
537            if (strlen($rdata) < 16) {
538                return $ret;
539            }
540
541            $data = unpack("Vreservedsize", $rdata);
542            $rdata = substr($rdata, 4 + $data["reservedsize"]);
543
544            for($i=0;$i<$nexceptions;$i++)
545            {
546                // subject and location in ucs-2 to utf-8
547                if ($writerversion >= 0x3009) {
548                    $data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4
549                    $rdata = substr($rdata, 4 + $data["size"]);
550                }
551
552                $data = unpack("Vreservedsize", $rdata);
553                $rdata = substr($rdata, 4 + $data["reservedsize"]);
554
555                // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
556                if ($exc_changed_details[$i]["bitmask"] & 0x11) {
557                    $data = unpack("Vstart/Vend/Vorig", $rdata);
558                    $rdata = substr($rdata, 4 * 3);
559
560                    $exc_changed_details[$i]["ex_start_datetime"] = $data["start"];
561                    $exc_changed_details[$i]["ex_end_datetime"] = $data["end"];
562                    $exc_changed_details[$i]["ex_orig_date"] = $data["orig"];
563                }
564
565                // ARO_SUBJECT
566                if ($exc_changed_details[$i]["bitmask"] & 0x01) {
567                    // decode ucs2 string to utf-8
568                    $data = unpack("vlength", $rdata);
569                    $rdata = substr($rdata, 2);
570                    $length = $data["length"];
571                    $data = substr($rdata, 0, $length * 2);
572                    $rdata = substr($rdata, $length * 2);
573                    $subject = iconv("UCS-2LE", "UTF-8", $data);
574                    // replace subject with unicode subject
575                    $exc_changed_details[$i]["subject"] = $subject;
576                }
577
578                // ARO_LOCATION
579                if ($exc_changed_details[$i]["bitmask"] & 0x10) {
580                    // decode ucs2 string to utf-8
581                    $data = unpack("vlength", $rdata);
582                    $rdata = substr($rdata, 2);
583                    $length = $data["length"];
584                    $data = substr($rdata, 0, $length * 2);
585                    $rdata = substr($rdata, $length * 2);
586                    $location = iconv("UCS-2LE", "UTF-8", $data);
587                    // replace subject with unicode subject
588                    $exc_changed_details[$i]["location"] = $location;
589                }
590
591                // ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
592                if ($exc_changed_details[$i]["bitmask"] & 0x11) {
593                    $data = unpack("Vreservedsize", $rdata);
594                    $rdata = substr($rdata, 4 + $data["reservedsize"]);
595                }
596            }
597
598            // update with extended data
599            $ret["changed_occurences"] = $exc_changed_details;
600
601            return $ret;
602        }
603
604        /**
605         * Saves the recurrence data to the recurrence property
606         * @param array $properties the recurrence data.
607         * @return string binary string
608         */
609        function saveRecurrence()
610        {
611            // Only save if a message was passed
612            if(!isset($this->message))
613                return;
614
615            // Abort if no recurrence was set
616            if(!isset($this->recur["type"]) && !isset($this->recur["subtype"])) {
617                return;
618            }
619
620            if(!isset($this->recur["start"]) && !isset($this->recur["end"])) {
621                return;
622            }
623
624            if(!isset($this->recur["startocc"]) && !isset($this->recur["endocc"])) {
625                return;
626            }
627
628            $rdata = pack("CCCCCCV", 0x04, 0x30, 0x04, 0x30, (int) $this->recur["type"], 0x20, (int) $this->recur["subtype"]);
629
630            $weekstart = 1; //monday
631            $forwardcount = 0;
632            $restocc = 0;
633            $dayofweek = (int) gmdate("w", (int) $this->recur["start"]); //0 (for Sunday) through 6 (for Saturday)
634
635            $term = (int) $this->recur["type"];
636            switch($term)
637            {
638                case 0x0A:
639                    // Daily
640                    if(!isset($this->recur["everyn"])) {
641                        return;
642                    }
643
644                    if($this->recur["subtype"] == 1) {
645
646                        // Daily every workday
647                        $rdata .= pack("VVVV", (6 * 24 * 60), 1, 0, 0x3E);
648                    } else {
649                        // Daily every N days (everyN in minutes)
650
651                        $everyn =  ((int) $this->recur["everyn"]) / 1440;
652
653                        // Calc first occ
654                        $firstocc = $this->unixDataToRecurData($this->recur["start"]) % ((int) $this->recur["everyn"]);
655
656                        $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], $this->recur["regen"] ? 1 : 0);
657                    }
658                    break;
659                case 0x0B:
660                    // Weekly
661                    if(!isset($this->recur["everyn"])) {
662                        return;
663                    }
664
665                    if (!$this->recur["regen"] && !isset($this->recur["weekdays"])) {
666                        return;
667                    }
668
669                    // No need to calculate startdate if sliding flag was set.
670                    if (!$this->recur['regen']) {
671                        // Calculate start date of recurrence
672
673                        // Find the first day that matches one of the weekdays selected
674                        $daycount = 0;
675                        $dayskip = -1;
676                        for($j = 0; $j < 7; $j++) {
677                            if(((int) $this->recur["weekdays"]) & (1<<( ($dayofweek+$j)%7)) ) {
678                                if($dayskip == -1)
679                                    $dayskip = $j;
680
681                                $daycount++;
682                            }
683                        }
684
685                        // $dayskip is the number of days to skip from the startdate until the first occurrence
686                        // $daycount is the number of days per week that an occurrence occurs
687
688                        $weekskip = 0;
689                        if(($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek+$dayskip) > 6)
690                            $weekskip = 1;
691
692                        // Check if the recurrence ends after a number of occurences, in that case we must calculate the
693                        // remaining occurences based on the start of the recurrence.
694                        if (((int) $this->recur["term"]) == 0x22) {
695                            // $weekskip is the amount of weeks to skip from the startdate before the first occurence
696                            // $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that
697                            // is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over
698                            // (eg when numoccur = 2, and daycount = 1)
699                            $forwardcount = floor( (int) ($this->recur["numoccur"] -1 ) / $daycount);
700
701                            // $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one
702                            // for the occurrence on the first day
703                            $restocc = ((int) $this->recur["numoccur"]) - ($forwardcount*$daycount) - 1;
704
705                            // $forwardcount is now the number of weeks we can go forward and still be inside the recurrence
706                            $forwardcount *= (int) $this->recur["everyn"];
707                        }
708
709                        // The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week)
710                        $this->recur["start"] = ((int) $this->recur["start"]) + ($dayskip * 24*60*60)+ ($weekskip *(((int) $this->recur["everyn"]) - 1) * 7 * 24*60*60);
711                    }
712
713                    // Calc first occ
714                    $firstocc = ($this->unixDataToRecurData($this->recur["start"]) ) % ( ((int) $this->recur["everyn"]) * 7 * 24 * 60);
715
716                    $firstocc -= (((int) gmdate("w", (int) $this->recur["start"])) - 1) * 24 * 60;
717
718                    if ($this->recur["regen"])
719                        $rdata .= pack("VVV", $firstocc, (int) $this->recur["everyn"], 1);
720                    else
721                        $rdata .= pack("VVVV", $firstocc, (int) $this->recur["everyn"], 0, (int) $this->recur["weekdays"]);
722                    break;
723                case 0x0C:
724                    // Monthly
725                case 0x0D:
726                    // Yearly
727                    if(!isset($this->recur["everyn"])) {
728                        return;
729                    }
730                    if($term == 0x0D /*yearly*/ && !isset($this->recur["month"])) {
731                        return;
732                    }
733
734                    if($term == 0x0C /*monthly*/) {
735                        $everyn = (int) $this->recur["everyn"];
736                    }else {
737                        $everyn = $this->recur["regen"] ? ((int) $this->recur["everyn"]) * 12 : 12;
738                    }
739
740                    // Get montday/month/year of original start
741                    $curmonthday = gmdate("j", (int) $this->recur["start"] );
742                    $curyear = gmdate("Y", (int) $this->recur["start"] );
743                    $curmonth = gmdate("n", (int) $this->recur["start"] );
744
745                    // Check if the recurrence ends after a number of occurences, in that case we must calculate the
746                    // remaining occurences based on the start of the recurrence.
747                    if (((int) $this->recur["term"]) == 0x22) {
748                        // $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus
749                        // one to make sure there are always at least one occurrence left)
750                        $forwardcount = ((((int) $this->recur["numoccur"])-1) * $everyn );
751                    }
752
753                    // Get month for yearly on D'th day of month M
754                    if($term == 0x0D /*yearly*/) {
755                        $selmonth = floor(((int) $this->recur["month"]) / (24 * 60 *29)) + 1; // 1=jan, 2=feb, eg
756                    }
757
758                    switch((int) $this->recur["subtype"])
759                    {
760                        // on D day of every M month
761                        case 2:
762                            if(!isset($this->recur["monthday"])) {
763                                return;
764                            }
765                            // Recalc startdate
766
767                            // Set on the right begin day
768
769                            // Go the beginning of the month
770                            $this->recur["start"] -= ($curmonthday-1) * 24*60*60;
771                            // Go the the correct month day
772                            $this->recur["start"] += (((int) $this->recur["monthday"])-1) * 24*60*60;
773
774                            // If the previous calculation gave us a start date *before* the original start date, then we need to skip to the next occurrence
775                            if ( ($term == 0x0C /*monthly*/ && ((int) $this->recur["monthday"]) < $curmonthday) ||
776                                ($term == 0x0D /*yearly*/ &&( $selmonth < $curmonth || ($selmonth == $curmonth && ((int) $this->recur["monthday"]) < $curmonthday)) ))
777                            {
778                                if($term == 0x0D /*yearly*/)
779                                    $count = ($everyn - ($curmonth - $selmonth)); // Yearly, go to next occurrence in 'everyn' months minus difference in first occurence and original date
780                                else
781                                    $count = $everyn; // Monthly, go to next occurrence in 'everyn' months
782
783                                // Forward by $count months. This is done by getting the number of days in that month and forwarding that many days
784                                for($i=0; $i < $count; $i++) {
785                                    $this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth);
786
787                                    if($curmonth == 12) {
788                                        $curyear++;
789                                        $curmonth = 0;
790                                    }
791                                    $curmonth++;
792                                }
793                            }
794
795                            // "start" is now pointing to the first occurrence, except that it will overshoot if the
796                            // month in which it occurs has less days than specified as the day of the month. So 31st
797                            // of each month will overshoot in february (29 days). We compensate for that by checking
798                            // if the day of the month we got is wrong, and then back up to the last day of the previous
799                            // month.
800                            if(((int) $this->recur["monthday"]) >=28 && ((int) $this->recur["monthday"]) <= 31 &&
801                                gmdate("j", ((int) $this->recur["start"])) < ((int) $this->recur["monthday"]))
802                            {
803                                $this->recur["start"] -= gmdate("j", ((int) $this->recur["start"])) * 24 * 60 *60;
804                            }
805
806                            // "start" is now the first occurrence
807
808                            if($term == 0x0C /*monthly*/) {
809                                // Calc first occ
810                                $monthIndex = ((((12%$everyn) * ((((int) gmdate("Y", $this->recur["start"])) - 1601)%$everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1))%$everyn;
811
812                                $firstocc = 0;
813                                for($i=0; $i < $monthIndex; $i++) {
814                                    $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), ($i%12)+1) / 60;
815                                }
816
817                                $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
818                            } else{
819                                // Calc first occ
820                                $firstocc = 0;
821                                $monthIndex = (int) gmdate("n", $this->recur["start"]);
822                                for($i=1; $i < $monthIndex; $i++) {
823                                    $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), $i) / 60;
824                                }
825
826                                $rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], (int) $this->recur["monthday"]);
827                            }
828                            break;
829
830                        case 3:
831                            // monthly: on Nth weekday of every M month
832                            // yearly: on Nth weekday of M month
833                            if(!isset($this->recur["weekdays"]) && !isset($this->recur["nday"])) {
834                                return;
835                            }
836
837                            $weekdays = (int) $this->recur["weekdays"];
838                            $nday = (int) $this->recur["nday"];
839
840                            // Calc startdate
841                            $monthbegindow = (int) $this->recur["start"];
842
843                            if($nday == 5) {
844                                // Set date on the last day of the last month
845                                $monthbegindow += (gmdate("t", $monthbegindow ) - gmdate("j", $monthbegindow )) * 24 * 60 * 60;
846                            }else {
847                                // Set on the first day of the month
848                                $monthbegindow -= ((gmdate("j", $monthbegindow )-1) * 24 * 60 * 60);
849                            }
850
851                            if($term == 0x0D /*yearly*/) {
852                                // Set on right month
853                                if($selmonth < $curmonth)
854                                    $tmp = 12 - $curmonth + $selmonth;
855                                else
856                                    $tmp = ($selmonth - $curmonth);
857
858                                for($i=0; $i < $tmp; $i++) {
859                                    $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
860
861                                    if($curmonth == 12) {
862                                        $curyear++;
863                                        $curmonth = 0;
864                                    }
865                                    $curmonth++;
866                                }
867
868                            }else {
869                                // Check or you exist in the right month
870
871                                for($i = 0; $i < 7; $i++) {
872                                    if($nday == 5 && (1<<( (gmdate("w", $monthbegindow)-$i)%7) ) & $weekdays) {
873                                        $day = gmdate("j", $monthbegindow) - $i;
874                                        break;
875                                    }else if($nday != 5 && (1<<( (gmdate("w", $monthbegindow )+$i)%7) ) & $weekdays) {
876                                        $day = (($nday-1)*7) + ($i+1);
877                                        break;
878                                    }
879                                }
880
881                                // Goto the next X month
882                                if(isset($day) && ($day < gmdate("j", (int) $this->recur["start"])) ) {
883                                    if($nday == 5) {
884                                        $monthbegindow += 24 * 60 * 60;
885                                        if($curmonth == 12) {
886                                            $curyear++;
887                                            $curmonth = 0;
888                                        }
889                                        $curmonth++;
890                                    }
891
892                                    for($i=0; $i < $everyn; $i++) {
893                                        $monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
894
895                                        if($curmonth == 12) {
896                                            $curyear++;
897                                            $curmonth = 0;
898                                        }
899                                        $curmonth++;
900                                    }
901
902                                    if($nday == 5) {
903                                        $monthbegindow -= 24 * 60 * 60;
904                                    }
905                                }
906                            }
907
908                            //FIXME: weekstart?
909
910                            $day = 0;
911                            // Set start on the right day
912                            for($i = 0; $i < 7; $i++) {
913                                if($nday == 5 && (1<<( (gmdate("w", $monthbegindow )-$i)%7) ) & $weekdays) {
914                                    $day = $i;
915                                    break;
916                                }else if($nday != 5 && (1<<( (gmdate("w", $monthbegindow )+$i)%7) ) & $weekdays) {
917                                    $day = ($nday - 1) * 7 + ($i+1);
918                                    break;
919                                }
920                            }
921                            if($nday == 5)
922                                $monthbegindow -= $day * 24 * 60 *60;
923                            else
924                                $monthbegindow += ($day-1) * 24 * 60 *60;
925
926                            $firstocc = 0;
927
928                            if($term == 0x0C /*monthly*/) {
929                                // Calc first occ
930                                $monthIndex = ((((12%$everyn) * (((int) gmdate("Y", $this->recur["start"]) - 1601)%$everyn)) % $everyn) + (((int) gmdate("n", $this->recur["start"])) - 1))%$everyn;
931
932                                for($i=0; $i < $monthIndex; $i++) {
933                                    $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), ($i%12)+1) / 60;
934                                }
935
936                                $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
937                            } else {
938                                // Calc first occ
939                                $monthIndex = (int) gmdate("n", $this->recur["start"]);
940
941                                for($i=1; $i < $monthIndex; $i++) {
942                                    $firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), $i) / 60;
943                                }
944
945                                $rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
946                            }
947                            break;
948                    }
949                    break;
950
951
952
953            }
954
955            if(!isset($this->recur["term"])) {
956                return;
957            }
958
959            // Terminate
960            $term = (int) $this->recur["term"];
961            $rdata .= pack("CCCC", $term, 0x20, 0x00, 0x00);
962
963            switch($term)
964            {
965                // After the given enddate
966                case 0x21:
967                    $rdata .= pack("V", 10);
968                    break;
969                // After a number of times
970                case 0x22:
971                    if(!isset($this->recur["numoccur"])) {
972                        return;
973                    }
974
975                    $rdata .= pack("V", (int) $this->recur["numoccur"]);
976                    break;
977                // Never ends
978                case 0x23:
979                    $rdata .= pack("V", 0);
980                    break;
981            }
982
983            // Strange little thing for the recurrence type "every workday"
984            if(((int) $this->recur["type"]) == 0x0B && ((int) $this->recur["subtype"]) == 1) {
985                $rdata .= pack("V", 1);
986            } else { // Other recurrences
987                $rdata .= pack("V", 0);
988            }
989
990            // Exception data
991
992            // Get all exceptions
993            $deleted_items = $this->recur["deleted_occurences"];
994            $changed_items = $this->recur["changed_occurences"];
995
996            // Merge deleted and changed items into one list
997            $items = $deleted_items;
998
999            foreach($changed_items as $changed_item)
1000                array_push($items, $changed_item["basedate"]);
1001
1002            sort($items);
1003
1004            // Add the merged list in to the rdata
1005            $rdata .= pack("V", count($items));
1006            foreach($items as $item)
1007                $rdata .= pack("V", $this->unixDataToRecurData($item));
1008
1009            // Loop through the changed exceptions (not deleted)
1010            $rdata .= pack("V", count($changed_items));
1011            $items = array();
1012
1013            foreach($changed_items as $changed_item)
1014            {
1015                $items[] = $this->dayStartOf($changed_item["start"]);
1016            }
1017
1018            sort($items);
1019
1020            // Add the changed items list int the rdata
1021            foreach($items as $item)
1022                $rdata .= pack("V", $this->unixDataToRecurData($item));
1023
1024            // Set start date
1025            $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["start"]));
1026
1027            // Set enddate
1028            switch($term)
1029            {
1030                // After the given enddate
1031                case 0x21:
1032                    $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]));
1033                    break;
1034                // After a number of times
1035                case 0x22:
1036                    // @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour
1037                    $occenddate = (int) $this->recur["start"];
1038
1039                    switch((int) $this->recur["type"]) {
1040                        case 0x0A: //daily
1041
1042                            if($this->recur["subtype"] == 1) {
1043                                // Daily every workday
1044                                $restocc = (int) $this->recur["numoccur"];
1045
1046                                // Get starting weekday
1047                                $nowtime = $this->gmtime($occenddate);
1048                                $j = $nowtime["tm_wday"];
1049
1050                                while(1)
1051                                {
1052                                    if(($j%7) > 0 && ($j%7)<6 ) {
1053                                        $restocc--;
1054                                    }
1055
1056                                    $j++;
1057
1058                                    if($restocc <= 0)
1059                                        break;
1060
1061                                    $occenddate += 24*60*60;
1062                                }
1063
1064                            } else {
1065                                // -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence)
1066                                $occenddate += (((int) $this->recur["everyn"]) * 60 * (((int) $this->recur["numoccur"]-1)));
1067                            }
1068                            break;
1069                        case 0x0B: //weekly
1070                            // Needed values
1071                            // $forwardcount - number of weeks we can skip forward
1072                            // $restocc - number of remaning occurrences after the week skip
1073
1074                            // Add the weeks till the last item
1075                            $occenddate+=($forwardcount*7*24*60*60);
1076
1077                            $dayofweek = gmdate("w", $occenddate);
1078
1079                            // Loop through the last occurrences until we have had them all
1080                            for($j = 1; $restocc>0; $j++)
1081                            {
1082                                // Jump to the next week (which may be N weeks away) when going over the week boundary
1083                                if((($dayofweek+$j)%7) == $weekstart)
1084                                    $occenddate += (((int) $this->recur["everyn"])-1) * 7 * 24*60*60;
1085
1086                                // If this is a matching day, once less occurrence to process
1087                                if(((int) $this->recur["weekdays"]) & (1<<(($dayofweek+$j)%7)) ) {
1088                                    $restocc--;
1089                                }
1090
1091                                // Next day
1092                                $occenddate += 24*60*60;
1093                            }
1094
1095                            break;
1096                        case 0x0C: //monthly
1097                        case 0x0D: //yearly
1098
1099                            $curyear = gmdate("Y", (int) $this->recur["start"] );
1100                            $curmonth = gmdate("n", (int) $this->recur["start"] );
1101                            // $forwardcount = months
1102
1103                            switch((int) $this->recur["subtype"])
1104                            {
1105                                case 2: // on D day of every M month
1106                                    while($forwardcount > 0)
1107                                    {
1108                                        $occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1109
1110                                        if($curmonth >=12) {
1111                                            $curmonth = 1;
1112                                            $curyear++;
1113                                        } else {
1114                                            $curmonth++;
1115                                        }
1116                                        $forwardcount--;
1117                                    }
1118
1119                                    // compensation between 28 and 31
1120                                    if(((int) $this->recur["monthday"]) >=28 && ((int) $this->recur["monthday"]) <= 31 &&
1121                                        gmdate("j", $occenddate) < ((int) $this->recur["monthday"]))
1122                                    {
1123                                        if(gmdate("j", $occenddate) < 28)
1124                                            $occenddate -= gmdate("j", $occenddate) * 24 * 60 *60;
1125                                        else
1126                                            $occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 *60;
1127                                    }
1128
1129
1130                                    break;
1131                                case 3: // on Nth weekday of every M month
1132                                    $nday = (int) $this->recur["nday"]; //1 tot 5
1133                                    $weekdays = (int) $this->recur["weekdays"];
1134
1135
1136                                    while($forwardcount > 0)
1137                                    {
1138                                        $occenddate += $this->getMonthInSeconds($curyear, $curmonth);
1139                                        if($curmonth >=12) {
1140                                            $curmonth = 1;
1141                                            $curyear++;
1142                                        } else {
1143                                            $curmonth++;
1144                                        }
1145
1146                                        $forwardcount--;
1147                                    }
1148
1149                                    if($nday == 5) {
1150                                        // Set date on the last day of the last month
1151                                        $occenddate += (gmdate("t", $occenddate ) - gmdate("j", $occenddate )) * 24 * 60 * 60;
1152                                    }else {
1153                                        // Set date on the first day of the last month
1154                                        $occenddate -= (gmdate("j", $occenddate )-1) * 24 * 60 * 60;
1155                                    }
1156
1157                                    for($i = 0; $i < 7; $i++) {
1158                                        if( $nday == 5 && (1<<( (gmdate("w", $occenddate)-$i)%7) ) & $weekdays) {
1159                                            $occenddate -= $i * 24 * 60 * 60;
1160                                            break;
1161                                        }else if($nday != 5 && (1<<( (gmdate("w", $occenddate)+$i)%7) ) & $weekdays) {
1162                                            $occenddate +=  ($i + (($nday-1) *7)) * 24 * 60 * 60;
1163                                            break;
1164                                        }
1165                                    }
1166
1167                                break; //case 3:
1168                                }
1169
1170                            break;
1171
1172                    }
1173
1174                    if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX)
1175                        $occenddate = PHP_INT_MAX;
1176
1177                    $this->recur["end"] = $occenddate;
1178
1179                    $rdata .= pack("V", $this->unixDataToRecurData((int) $this->recur["end"]) );
1180                    break;
1181                // Never ends
1182                case 0x23:
1183                default:
1184                    $this->recur["end"] = 0x7fffffff; // max date -> 2038
1185                    $rdata .= pack("V", 0x5AE980DF);
1186                    break;
1187            }
1188
1189            // UTC date
1190            $utcstart = $this->toGMT($this->tz, (int) $this->recur["start"]);
1191            $utcend = $this->toGMT($this->tz, (int) $this->recur["end"]);
1192
1193            //utc date+time
1194            $utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (((int) $this->recur["startocc"])*60) : $utcstart;
1195            $utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (((int) $this->recur["endocc"]) * 60) : $utcstart;
1196
1197            // update reminder time
1198            mapi_setprops($this->message, Array($this->proptags["reminder_time"] => $utcfirstoccstartdatetime ));
1199
1200            // update first occurrence date
1201            mapi_setprops($this->message, Array($this->proptags["startdate"] => $utcfirstoccstartdatetime ));
1202            mapi_setprops($this->message, Array($this->proptags["duedate"] => $utcfirstoccenddatetime ));
1203            mapi_setprops($this->message, Array($this->proptags["commonstart"] => $utcfirstoccstartdatetime ));
1204            mapi_setprops($this->message, Array($this->proptags["commonend"] => $utcfirstoccenddatetime ));
1205
1206            // Set Outlook properties, if it is an appointment
1207            if (isset($this->recur["message_class"]) && $this->recur["message_class"] == "IPM.Appointment") {
1208                // update real begin and real end date
1209                mapi_setprops($this->message, Array($this->proptags["startdate_recurring"] => $utcstart));
1210                mapi_setprops($this->message, Array($this->proptags["enddate_recurring"] => $utcend));
1211
1212                // recurrencetype
1213                // Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype
1214                mapi_setprops($this->message, Array($this->proptags["recurrencetype"] => ((int) $this->recur["type"]) - 0x9));
1215
1216                // set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting
1217                mapi_setprops($this->message, Array($this->proptags["side_effects"] => 369));
1218            } else {
1219                mapi_setprops($this->message, Array($this->proptags["side_effects"] => 3441));
1220            }
1221
1222            // FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog
1223            // Any change of the recurrence (including changing and deleting exceptions) causes the flagdueby to be reset
1224            // to the 'next' occurrence; this makes sure that deleting the next ocurrence will correctly set the reminder to
1225            // the occurrence after that. The 'next' occurrence is defined as being the first occurrence that starts at moment X (server time)
1226            // with the reminder flag set.
1227            $reminderprops = mapi_getprops($this->message, array($this->proptags["reminder_minutes"]) );
1228            if(isset($reminderprops[$this->proptags["reminder_minutes"]]) ) {
1229                $occ = false;
1230                $occurrences = $this->getItems(time(), 0x7ff00000, 3, true);
1231
1232                for($i = 0, $len = count($occurrences) ; $i < $len; $i++) {
1233                    // This will actually also give us appointments that have already started, but not yet ended. Since we want the next
1234                    // reminder that occurs after time(), we may have to skip the first few entries. We get 3 entries since that is the maximum
1235                    // number that would be needed (assuming reminder for item X cannot be before the previous occurrence starts). Worst case:
1236                    // time() is currently after start but before end of item, but reminder of next item has already passed (reminder for next item
1237                    // can be DURING the previous item, eg daily allday events). In that case, the first and second items must be skipped.
1238
1239                    if(($occurrences[$i][$this->proptags["startdate"]] - $reminderprops[$this->proptags["reminder_minutes"]] * 60) > time()) {
1240                        $occ = $occurrences[$i];
1241                        break;
1242                    }
1243                }
1244
1245                if($occ) {
1246                    mapi_setprops($this->message, Array($this->proptags["flagdueby"] => $occ[$this->proptags["startdate"]] - ($reminderprops[$this->proptags["reminder_minutes"]] * 60) ));
1247                } else {
1248                    // Last reminder passed, no reminders any more.
1249                    mapi_setprops($this->message, Array($this->proptags["reminder"] => false, $this->proptags["flagdueby"] => 0x7ff00000));
1250                }
1251            }
1252
1253            // Default data
1254            // Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information)
1255            $rdata .= pack("VCCCC", 0x00003006, 0x08, 0x30, 0x00, 0x00);
1256
1257            if(isset($this->recur["startocc"]) && isset($this->recur["endocc"])) {
1258                // Set start and endtime in minutes
1259                $rdata .= pack("VV", (int) $this->recur["startocc"], (int) $this->recur["endocc"]);
1260            }
1261
1262            // Detailed exception data
1263
1264            $changed_items = $this->recur["changed_occurences"];
1265
1266            $rdata .= pack("v", count($changed_items));
1267
1268            foreach($changed_items as $changed_item)
1269            {
1270                // Set start and end time of exception
1271                $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1272                $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1273                $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1274
1275                //Bitmask
1276                $bitmask = 0;
1277
1278                // Check for changed strings
1279                if(isset($changed_item["subject"]))    {
1280                    $bitmask |= 1 << 0;
1281                }
1282
1283                if(isset($changed_item["remind_before"])) {
1284                    $bitmask |= 1 << 2;
1285                }
1286
1287                if(isset($changed_item["reminder_set"])) {
1288                    $bitmask |= 1 << 3;
1289                }
1290
1291                if(isset($changed_item["location"])) {
1292                    $bitmask |= 1 << 4;
1293                }
1294
1295                if(isset($changed_item["busystatus"])) {
1296                    $bitmask |= 1 << 5;
1297                }
1298
1299                if(isset($changed_item["alldayevent"])) {
1300                    $bitmask |= 1 << 7;
1301                }
1302
1303                if(isset($changed_item["label"])) {
1304                    $bitmask |= 1 << 8;
1305                }
1306
1307                $rdata .= pack("v", $bitmask);
1308
1309                // Set "subject"
1310                if(isset($changed_item["subject"])) {
1311                    // convert utf-8 to non-unicode blob string (us-ascii?)
1312                    $subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]);
1313                    $length = strlen($subject);
1314                    $rdata .= pack("vv", $length + 1, $length);
1315                    $rdata .= pack("a".$length, $subject);
1316                }
1317
1318                if(isset($changed_item["remind_before"])) {
1319                    $rdata .= pack("V", $changed_item["remind_before"]);
1320                }
1321
1322                if(isset($changed_item["reminder_set"])) {
1323                    $rdata .= pack("V", $changed_item["reminder_set"]);
1324                }
1325
1326                if(isset($changed_item["location"])) {
1327                    $location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]);
1328                    $length = strlen($location);
1329                    $rdata .= pack("vv", $length + 1, $length);
1330                    $rdata .= pack("a".$length, $location);
1331                }
1332
1333                if(isset($changed_item["busystatus"])) {
1334                    $rdata .= pack("V", $changed_item["busystatus"]);
1335                }
1336
1337                if(isset($changed_item["alldayevent"])) {
1338                    $rdata .= pack("V", $changed_item["alldayevent"]);
1339                }
1340
1341                if(isset($changed_item["label"])) {
1342                    $rdata .= pack("V", $changed_item["label"]);
1343                }
1344            }
1345
1346            $rdata .= pack("V", 0);
1347
1348            // write extended data
1349            foreach($changed_items as $changed_item)
1350            {
1351                $rdata .= pack("V", 0);
1352                if(isset($changed_item["subject"]) || isset($changed_item["location"])) {
1353                    $rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
1354                    $rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
1355                    $rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
1356                }
1357
1358                if(isset($changed_item["subject"])) {
1359                    $subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]);
1360                    $length = iconv_strlen($subject, "UCS-2LE");
1361                    $rdata .= pack("v", $length);
1362                    $rdata .= pack("a".$length*2, $subject);
1363                }
1364
1365                if(isset($changed_item["location"])) {
1366                    $location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]);
1367                    $length = iconv_strlen($location, "UCS-2LE");
1368                    $rdata .= pack("v", $length);
1369                    $rdata .= pack("a".$length*2, $location);
1370                }
1371
1372                if(isset($changed_item["subject"]) || isset($changed_item["location"])) {
1373                    $rdata .= pack("V", 0);
1374                }
1375            }
1376
1377            $rdata .= pack("V", 0);
1378
1379            // Set props
1380            mapi_setprops($this->message, Array($this->proptags["recurring_data"] => $rdata, $this->proptags["recurring"] => true));
1381            if(isset($this->tz) && $this->tz){
1382                $timezone = "GMT";
1383                if ($this->tz["timezone"]!=0){
1384                    // Create user readable timezone information
1385                    $timezone = sprintf("(GMT %s%02d:%02d)",    (-$this->tz["timezone"]>0 ? "+" : "-"),
1386                                                            abs($this->tz["timezone"]/60),
1387                                                            abs($this->tz["timezone"]%60));
1388                }
1389                mapi_setprops($this->message, Array($this->proptags["timezone_data"] => $this->getTimezoneData($this->tz),
1390                                                    $this->proptags["timezone"] => $timezone));
1391            }
1392        }
1393
1394        /**
1395        * Function which converts a recurrence date timestamp to an unix date timestamp.
1396        * @author Steve Hardy
1397        * @param Int $rdate the date which will be converted
1398        * @return Int the converted date
1399        */
1400        function recurDataToUnixData($rdate)
1401        {
1402            return ($rdate - 194074560) * 60 ;
1403        }
1404
1405        /**
1406        * Function which converts an unix date timestamp to recurrence date timestamp.
1407        * @author Johnny Biemans
1408        * @param Date $date the date which will be converted
1409        * @return Int the converted date in minutes
1410        */
1411        function unixDataToRecurData($date)
1412        {
1413            return ($date / 60) + 194074560;
1414        }
1415
1416        /**
1417        * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves
1418        * @author Steve Hardy
1419        */
1420        function GetTZOffset($ts)
1421        {
1422            $Offset = date("O", $ts);
1423
1424            $Parity = $Offset < 0 ? -1 : 1;
1425            $Offset = $Parity * $Offset;
1426            $Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;
1427
1428            return $Parity * $Offset;
1429        }
1430
1431        /**
1432        * gmtime() doesn't exist in standard PHP, so we have to implement it ourselves
1433        * @author Steve Hardy
1434        * @param Date $time
1435        * @return Date GMT Time
1436        */
1437        function gmtime($time)
1438        {
1439            $TZOffset = $this->GetTZOffset($time);
1440
1441            $t_time = $time - $TZOffset * 60; #Counter adjust for localtime()
1442            $t_arr = localtime($t_time, 1);
1443
1444            return $t_arr;
1445        }
1446
1447        function isLeapYear($year) {
1448            return ( $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0) );
1449        }
1450
1451        function getMonthInSeconds($year, $month)
1452        {
1453            if( in_array($month, array(1,3,5,7,8,10,12) ) ) {
1454                $day = 31;
1455            } else if( in_array($month, array(4,6,9,11) ) ) {
1456                $day = 30;
1457            } else {
1458                $day = 28;
1459                if( $this->isLeapYear($year) == 1 )
1460                    $day++;
1461            }
1462            return $day * 24 * 60 * 60;
1463        }
1464
1465        /**
1466         * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour
1467         * @param int $year
1468         * @param int $month
1469         * @param int $week
1470         * @param int $day
1471         * @param int $hour
1472         * @return returns the timestamp of the given date, timezone-independant
1473         */
1474        function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour)
1475        {
1476            // get first day of month
1477            $date = gmmktime(0,0,0,$month,0,$year + 1900);
1478
1479            // get wday info
1480            $gmdate = $this->gmtime($date);
1481
1482            $date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week
1483
1484            $date += $week * 7 * 24 * 60 * 60; // go to correct week nr
1485            $date += $day * 24 * 60 * 60;
1486            $date += $hour * 60 * 60;
1487
1488            $gmdate = $this->gmtime($date);
1489
1490            // if we are in the next month, then back up a week, because week '5' means
1491            // 'last week of month'
1492
1493            if($gmdate["tm_mon"]+1 != $month)
1494                $date -= 7 * 24 * 60 * 60;
1495
1496            return $date;
1497        }
1498
1499        /**
1500         * getTimezone gives the timezone offset (in minutes) of the given
1501         * local date/time according to the given TZ info
1502         */
1503        function getTimezone($tz, $date)
1504        {
1505            // No timezone -> GMT (+0)
1506            if(!isset($tz["timezone"]))
1507                return 0;
1508
1509            $dst = false;
1510            $gmdate = $this->gmtime($date);
1511
1512            $dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]);
1513            $dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]);
1514
1515            if($dststart <= $dstend) {
1516                // Northern hemisphere, eg DST is during Mar-Oct
1517                if($date > $dststart && $date < $dstend) {
1518                    $dst = true;
1519                }
1520            } else {
1521                // Southern hemisphere, eg DST is during Oct-Mar
1522                if($date < $dstend || $date > $dststart) {
1523                    $dst = true;
1524                }
1525            }
1526
1527            if($dst) {
1528                return $tz["timezone"] + $tz["timezonedst"];
1529            } else {
1530                return $tz["timezone"];
1531            }
1532        }
1533
1534        /**
1535         * getWeekNr() returns the week nr of the month (ie first week of february is 1)
1536         */
1537        function getWeekNr($date)
1538        {
1539            $gmdate = gmtime($date);
1540            $gmdate["tm_mday"] = 0;
1541            return strftime("%W", $date) - strftime("%W", gmmktime($gmdate)) + 1;
1542        }
1543
1544        /**
1545         * parseTimezone parses the timezone as specified in named property 0x8233
1546         * in Outlook calendar messages. Returns the timezone in minutes negative
1547         * offset (GMT +2:00 -> -120)
1548         */
1549        function parseTimezone($data)
1550        {
1551            if(strlen($data) < 48)
1552                return;
1553
1554            $tz = unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data);
1555            return $tz;
1556        }
1557
1558        function getTimezoneData($tz)
1559        {
1560            $data = pack("lllllvvllvlvvlv", $tz["timezone"], 0, $tz["timezonedst"], 0, $tz["dstendmonth"], $tz["dstendweek"], $tz["dstendhour"], 0, 0, 0, $tz["dststartmonth"], $tz["dststartweek"], $tz["dststarthour"], 0 ,0);
1561
1562            return $data;
1563        }
1564
1565        /**
1566         * createTimezone creates the timezone as specified in the named property 0x8233
1567         * see also parseTimezone()
1568         * $tz is an array with the timezone data
1569         */
1570        function createTimezone($tz)
1571        {
1572            $data = pack("lxxxxlxxxxlvvxxxxxxxxxxlvvxxxxxx",
1573                        $tz["timezone"],
1574                        array_key_exists("timezonedst",$tz)?$tz["timezonedst"]:0,
1575                        array_key_exists("dstendmonth",$tz)?$tz["dstendmonth"]:0,
1576                        array_key_exists("dstendweek",$tz)?$tz["dstendweek"]:0,
1577                        array_key_exists("dstendhour",$tz)?$tz["dstendhour"]:0,
1578                        array_key_exists("dststartmonth",$tz)?$tz["dststartmonth"]:0,
1579                        array_key_exists("dststartweek",$tz)?$tz["dststartweek"]:0,
1580                        array_key_exists("dststarthour",$tz)?$tz["dststarthour"]:0
1581                    );
1582
1583            return $data;
1584        }
1585
1586        /**
1587         * toGMT returns a timestamp in GMT time for the time and timezone given
1588         */
1589        function toGMT($tz, $date) {
1590            if(!isset($tz['timezone']))
1591                return $date;
1592            $offset = $this->getTimezone($tz, $date);
1593
1594            return $date + $offset * 60;
1595        }
1596
1597        /**
1598         * fromGMT returns a timestamp in the local timezone given from the GMT time given
1599         */
1600        function fromGMT($tz, $date) {
1601            $offset = $this->getTimezone($tz, $date);
1602
1603            return $date - $offset * 60;
1604        }
1605
1606        /**
1607         * Function to get timestamp of the beginning of the day of the timestamp given
1608         * @param date $date
1609         * @return date timestamp referring to same day but at 00:00:00
1610         */
1611        function dayStartOf($date)
1612        {
1613            $time1 = $this->gmtime($date);
1614
1615            return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900);
1616        }
1617
1618        /**
1619         * Function to get timestamp of the beginning of the month of the timestamp given
1620         * @param date $date
1621         * @return date Timestamp referring to same month but on the first day, and at 00:00:00
1622         */
1623        function monthStartOf($date)
1624        {
1625            $time1 = $this->gmtime($date);
1626
1627            return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900);
1628        }
1629
1630        /**
1631         * Function to get timestamp of the beginning of the year of the timestamp given
1632         * @param date $date
1633         * @return date Timestamp referring to the same year but on Jan 01, at 00:00:00
1634         */
1635        function yearStartOf($date)
1636        {
1637            $time1 = $this->gmtime($date);
1638
1639            return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900);
1640        }
1641
1642
1643        /**
1644         * Function which returns the items in a given interval. This included expansion of the recurrence and
1645         * processing of exceptions (modified and deleted).
1646         *
1647         * @param string $entryid the entryid of the message
1648         * @param array $props the properties of the message
1649         * @param date $start start time of the interval (GMT)
1650         * @param date $end end time of the interval (GMT)
1651         */
1652        function getItems($start, $end, $limit = 0, $remindersonly = false)
1653        {
1654            $items = array();
1655
1656            if(isset($this->recur)) {
1657
1658                // Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which
1659                // exceptions are in range and have a reminder set
1660                if($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) {
1661                    // Sort exceptions by start time
1662                    uasort($this->recur["changed_occurences"], array($this, "sortExceptionStart"));
1663
1664                    // Loop through all changed exceptions
1665                    foreach($this->recur["changed_occurences"] as $exception) {
1666                        // Check reminder set
1667                        if(!isset($exception["reminder"]) || $exception["reminder"] == false)
1668                            continue;
1669
1670                        // Convert to GMT
1671                        $occstart = $this->toGMT($this->tz, $exception["start"]); // seb changed $tz to $this->tz
1672                        $occend = $this->toGMT($this->tz, $exception["end"]); // seb changed $tz to $this->tz
1673
1674                        // Check range criterium
1675                        if($occstart > $end || $occend < $start)
1676                            continue;
1677
1678                        // OK, add to items.
1679                        array_push($items, $this->getExceptionProperties($exception));
1680                        if($limit && (count($items) == $limit))
1681                            break;
1682                    }
1683
1684                    uasort($items, array($this, "sortStarttime"));
1685
1686                    return $items;
1687                }
1688
1689                // From here on, the dates of the occurrences are calculated in local time, so the days we're looking
1690                // at are calculated from the local time dates of $start and $end
1691
1692                if ($this->recur['regen'] && isset($this->action['datecompleted'])) {
1693                    $daystart = $this->dayStartOf($this->action['datecompleted']);
1694                } else {
1695                    $daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence
1696                }
1697
1698                // Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view
1699                // or the end of the recurrence, whichever comes first
1700                if($end > $this->toGMT($this->tz, $this->recur["end"])) {
1701                    $rangeend = $this->toGMT($this->tz, $this->recur["end"]);
1702                } else {
1703                    $rangeend = $end;
1704                }
1705
1706                $dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend));
1707
1708                // Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range.
1709
1710                switch($this->recur["type"])
1711                {
1712                case 10:
1713                    // Daily
1714                    if($this->recur["everyn"] <= 0)
1715                        $this->recur["everyn"] = 1440;
1716
1717                    if($this->recur["subtype"] == 0) {
1718                        // Every Nth day
1719                        for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) {
1720                            $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1721                        }
1722                    } else {
1723                        // Every workday
1724                        for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440)
1725                        {
1726                            $nowtime = $this->gmtime($now);
1727                            if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace
1728                                $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1729                            }
1730                        }
1731                    }
1732                    break;
1733                case 11:
1734                    // Weekly
1735                    if($this->recur["everyn"] <= 0)
1736                        $this->recur["everyn"] = 1;
1737
1738                    // If sliding flag is set then move to 'n' weeks
1739                    if ($this->recur['regen']) $daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]);
1740
1741                    for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"]))
1742                    {
1743                        if ($this->recur['regen']) {
1744                            $this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1745                        } else {
1746                            // Loop through the whole following week to the first occurrence of the week, add each day that is specified
1747                            for($wday = 0; $wday < 7; $wday++)
1748                            {
1749                                $daynow = $now + $wday * 60 * 60 * 24;
1750                                //checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item
1751                                if ($daynow <= $dayend){
1752                                    $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1753                                    if(($this->recur["weekdays"] &(1 << $nowtime["tm_wday"]))) { // Selected ?
1754                                        $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1755                                    }
1756                                }
1757                            }
1758                        }
1759                    }
1760                    break;
1761                case 12:
1762                    // Monthly
1763                    if($this->recur["everyn"] <= 0)
1764                        $this->recur["everyn"] = 1;
1765
1766                    // Loop through all months from start to end of occurrence, starting at beginning of first month
1767                    for($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 )
1768                    {
1769                        if(isset($this->recur["monthday"]) &&($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months
1770                            $difference = 1;
1771                            if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) {
1772                                $difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1;
1773                            }
1774                            $daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60);
1775                            //checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item
1776                            if ($daynow <= $dayend){
1777                                $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1778                            }
1779                        }
1780                        else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] of every N months
1781                            // Sanitize input
1782                            if($this->recur["weekdays"] == 0)
1783                                $this->recur["weekdays"] = 1;
1784
1785                            // If nday is not set to the last day in the month
1786                            if ($this->recur["nday"] < 5) {
1787                                // keep the track of no. of time correct selection pattern(like 2nd weekday, 4th fiday, etc.)is matched
1788                                $ndaycounter = 0;
1789                                // Find matching weekday in this month
1790                                for($day = 0; $day < $this->daysInMonth($now, 1); $day++)
1791                                {
1792                                    $daynow = $now + $day * 60 * 60 * 24;
1793                                    $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1794
1795                                    if($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1796                                        $ndaycounter ++;
1797                                    }
1798                                    // check the selected pattern is same as asked Nth weekday,If so set the firstday
1799                                    if($this->recur["nday"] == $ndaycounter){
1800                                        $firstday = $day;
1801                                        break;
1802                                    }
1803                                }
1804                                // $firstday is the day of the month on which the asked pattern of nth weekday matches
1805                                $daynow = $now + $firstday * 60 * 60 * 24;
1806                            }else{
1807                                // Find last day in the month ($now is the firstday of the month)
1808                                $NumDaysInMonth =  $this->daysInMonth($now, 1);
1809                                $daynow = $now + (($NumDaysInMonth-1) * 24*60*60);
1810
1811                                $nowtime = $this->gmtime($daynow);
1812                                while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"]))==0){
1813                                    $daynow -= 86400;
1814                                    $nowtime = $this->gmtime($daynow);
1815                                }
1816                            }
1817
1818                            /**
1819                             * checks weather the next coming day in recurrence pattern is less than or equal to end day of the            * recurring item.Also check weather the coming day in recurrence pattern is greater than or equal to start * of recurring pattern, so that appointment that fall under the recurrence range are only displayed.
1820                             */
1821                            if ($daynow <= $dayend && $daynow >= $daystart){
1822                                $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz , $remindersonly);
1823                            }
1824                        } else if ($this->recur['regen']) {
1825                            $next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60);
1826                            $now = $daystart +($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60);
1827
1828                            if ($now <= $dayend) {
1829                                $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1830                            }
1831                        }
1832                    }
1833                    break;
1834                case 13:
1835                    // Yearly
1836                    if($this->recur["everyn"] <= 0)
1837                        $this->recur["everyn"] = 12;
1838
1839                    for($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 )
1840                    {
1841                        if(isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month
1842                            // recur["month"] is in minutes since the beginning of the year
1843                            $month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11]
1844                            $monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31]
1845                            $monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month
1846                            if($monthday > $this->daysInMonth($monthstart, 1))
1847                                $monthday = $this->daysInMonth($monthstart, 1);    // Cap $monthday on month length (eg 28 feb instead of 29 feb)
1848                            $daynow = $monthstart + ($monthday-1) * 24 * 60 * 60;
1849                            $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1850                        }
1851                        else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] in month X of every N years
1852
1853                            // Go the correct month
1854                            $monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60;
1855
1856                            // Find first matching weekday in this month
1857                            for($wday = 0; $wday < 7; $wday++)
1858                            {
1859                                $daynow = $monthnow + $wday * 60 * 60 * 24;
1860                                $nowtime = $this->gmtime($daynow); // Get the weekday of the current day
1861
1862                                if($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
1863                                    $firstday = $wday;
1864                                    break;
1865                                }
1866                            }
1867
1868                            // Same as above (monthly)
1869                            $daynow = $monthnow + ($firstday + ($this->recur["nday"]-1)*7) * 60 * 60 * 24;
1870
1871                            while($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) {
1872                                $daynow -= 7 * 60 * 60 * 24;
1873                            }
1874
1875                            $this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1876                        } else if ($this->recur['regen']) {
1877                            $year_starttime = $this->gmtime($now);
1878                            $is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1);    // +1 next year
1879                            $now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /*year in seconds*/);
1880
1881                            if ($now <= $dayend) {
1882                                $this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
1883                            }
1884                        }
1885                    }
1886                }
1887                //to get all exception items
1888                if (!empty($this->recur['changed_occurences']))
1889                    $this->processExceptionItems($items, $start, $end);
1890            }
1891
1892            // sort items on starttime
1893            usort($items, array($this, "sortStarttime"));
1894
1895            // Return the MAPI-compatible list of items for this object
1896            return $items;
1897        }
1898
1899        function sortStarttime($a, $b)
1900        {
1901            $aTime = $a[$this->proptags["startdate"]];
1902            $bTime = $b[$this->proptags["startdate"]];
1903
1904            return $aTime==$bTime?0:($aTime>$bTime?1:-1);
1905        }
1906
1907        /**
1908         * daysInMonth
1909         *
1910         * Returns the number of days in the upcoming number of months. If you specify 1 month as
1911         * $months it will give you the number of days in the month of $date. If you specify more it
1912         * will also count the days in the upcomming months and add that to the number of days. So
1913         * if you have a date in march and you specify $months as 2 it will return 61.
1914         * @param Integer $date Specified date as timestamp from which you want to know the number
1915         * of days in the month.
1916         * @param Integer $months Number of months you want to know the number of days in.
1917         * @returns Integer Number of days in the specified amount of months.
1918         */
1919        function daysInMonth($date, $months) {
1920            $days = 0;
1921
1922            for($i=0;$i<$months;$i++) {
1923                $days += date("t", $date + $days * 24 * 60 * 60);
1924            }
1925
1926            return $days;
1927        }
1928
1929        // Converts MAPI-style 'minutes' into the month of the year [0..11]
1930        function monthOfYear($minutes) {
1931            $d = gmmktime(0,0,0,1,1,2001); // The year 2001 was a non-leap year, and the minutes provided are always in non-leap-year-minutes
1932
1933            $d += $minutes*60;
1934
1935            $dtime = $this->gmtime($d);
1936
1937            return $dtime["tm_mon"];
1938        }
1939
1940        function sortExceptionStart($a, $b)
1941        {
1942            return $a["start"] == $b["start"] ? 0 : ($a["start"]  > $b["start"] ? 1 : -1 );
1943        }
1944    }
1945?>
Note: See TracBrowser for help on using the repository browser.