<?php
/*
 * Copyright 2005 - 2009  Zarafa B.V.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3, 
 * as published by the Free Software Foundation with the following additional 
 * term according to sec. 7:
 *  
 * According to sec. 7 of the GNU Affero General Public License, version
 * 3, the terms of the AGPL are supplemented with the following terms:
 * 
 * "Zarafa" is a registered trademark of Zarafa B.V. The licensing of
 * the Program under the AGPL does not imply a trademark license.
 * Therefore any rights, title and interest in our trademarks remain
 * entirely with us.
 * 
 * However, if you propagate an unmodified version of the Program you are
 * allowed to use the term "Zarafa" to indicate that you distribute the
 * Program. Furthermore you may use our trademarks where it is necessary
 * to indicate the intended purpose of a product or service provided you
 * use it in accordance with honest practices in industrial or commercial
 * matters.  If you want to propagate modified versions of the Program
 * under the name "Zarafa" or "Zarafa Server", you may only do so if you
 * have a written permission by Zarafa B.V. (to acquire a permission
 * please contact Zarafa at trademark@zarafa.com).
 * 
 * The interactive user interface of the software displays an attribution
 * notice containing the term "Zarafa" and/or the logo of Zarafa.
 * Interactive user interfaces of unmodified and modified versions must
 * display Appropriate Legal Notices according to sec. 5 of the GNU
 * Affero General Public License, version 3, when you propagate
 * unmodified or modified versions of the Program. In accordance with
 * sec. 7 b) of the GNU Affero General Public License, version 3, these
 * Appropriate Legal Notices must retain the logo of Zarafa or display
 * the words "Initial Development by Zarafa" if the display of the logo
 * is not reasonably feasible for technical reasons. The use of the logo
 * of Zarafa in Legal Notices is allowed for unmodified and modified
 * versions of the software.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *  
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

?>
<?php
	/**
	 * BaseRecurrence
	 * this class is superclass for recurrence for appointments and tasks. This class provides all
	 * basic features of recurrence.
	 */
	class BaseRecurrence
	{
		/**
		 * @var object Mapi Message Store (may be null if readonly)
		 */
		var $store;
		
		/**
		 * @var object Mapi Message (may be null if readonly)
		 */
		var $message;
		
		/**
		 * @var array Message Properties
		 */
		var $messageprops;

		/**
		 * @var array list of property tags
		 */
		var $proptags;

		/**
		 * @var recurrence data of this calendar item
		 */
		var $recur;

		/**
		 * Constructor
		 * @param resource $store MAPI Message Store Object
		 * @param resource $message the MAPI (appointment) message
		 * @param array $properties the list of MAPI properties the message has.
		 */
		function BaseRecurrence($store, $message)
		{
			$this->store = $store;

			if(is_array($message)) {
				$this->messageprops = $message;
			} else {
				$this->message = $message;
				$this->messageprops = mapi_getprops($this->message, $this->proptags);
			}

			if(isset($this->messageprops[$this->proptags["recurring_data"]]))
				// There is a possibility that recurr blob can be more than 255 bytes so get full blob through stream interface
				if (strlen($this->messageprops[$this->proptags["recurring_data"]]) >= 255) {
					$this->getFullRecurrenceBlob();
				}

				$this->recur = $this->parseRecurrence($this->messageprops[$this->proptags["recurring_data"]]);
			if(isset($this->proptags["timezone_data"]) && isset($this->messageprops[$this->proptags["timezone_data"]]))
				$this->tz = $this->parseTimezone($this->messageprops[$this->proptags["timezone_data"]]);
		}

		function getRecurrence()
		{
			return $this->recur;
		}

		function getFullRecurrenceBlob()
		{
			$message = mapi_msgstore_openentry($this->store, $this->messageprops[PR_ENTRYID]);

			$recurrBlob = '';
			$stream = mapi_openproperty($message, $this->proptags["recurring_data"], IID_IStream, 0, 0);
			$stat = mapi_stream_stat($stream);

			for ($i = 0; $i < $stat['cb']; $i += 1024)
				$recurrBlob .= mapi_stream_read($stream, 1024);

			if (!empty($recurrBlob)) $this->messageprops[$this->proptags["recurring_data"]] = $recurrBlob;
		}

		/**
		* Function for parsing the Recurrence value of a Calendar item.
		*
		* Retrieve it from Named Property 0x8216 as a PT_BINARY and pass the
		* data to this function
		*
		* Returns a structure containing the data:
		*
		* type		- type of recurrence: day=10, week=11, month=12, year=13
		* subtype	- type of day recurrence: 2=monthday (ie 21st day of month), 3=nday'th weekdays (ie. 2nd Tuesday and Wednesday)
		* start	- unix timestamp of first occurrence
		* end		- unix timestamp of last occurrence (up to and including), so when start == end -> occurrences = 1
		* numoccur     - occurrences (may be very large when there is no end data)
		*
		* then, for each type:
		*
		* Daily:
		*  everyn	- every [everyn] days in minutes
		*  regen	- regenerating event (like tasks)
		* 
		* Weekly:
		*  everyn	- every [everyn] weeks in weeks
		*  regen	- regenerating event (like tasks)
		*  weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
		*  
		* Monthly:
		*  everyn	- every [everyn] months
		*  regen	- regenerating event (like tasks)
		*  
		*  subtype 2:
		*	  monthday - on day [monthday] of the month
		*
		*  subtype 3:
		*	  weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
		*   nday	- on [nday]'th [weekdays] of the month
		*
		* Yearly:
		*  everyn	- every [everyn] months (12, 24, 36, ...)
		*  month	- in month [month] (although the month is encoded in minutes since the startning of the year ........)
		*  regen	- regenerating event (like tasks)
		*
		*  subtype 2:
		*   monthday - on day [monthday] of the month
		*
		*  subtype 3:
		*   weekdays - bitmask of week days, where each bit is one weekday (weekdays & 1 = Sunday, weekdays & 2 = Monday, etc)
		*	  nday	- on [nday]'th [weekdays] of the month [month]
		* @param string $rdata Binary string
		* @return array recurrence data.
		*/
		function parseRecurrence($rdata)
		{
			if (strlen($rdata) < 10) {
				return;
			}
			
			$ret["changed_occurences"] = array();
			$ret["deleted_occurences"] = array();
			
			$data = unpack("Vconst1/Crtype/Cconst2/Vrtype2", $rdata);
		
			$ret["type"] = $data["rtype"];
			$ret["subtype"] = $data["rtype2"];
			$rdata = substr($rdata, 10);
		
			switch ($data["rtype"])
			{
				case 0x0a: 
					// Daily
					if (strlen($rdata) < 12) {
						return $ret;
					}
		
					$data = unpack("Vunknown/Veveryn/Vregen", $rdata);
					$ret["everyn"] = $data["everyn"];
					$ret["regen"] = $data["regen"];
		
					switch($ret["subtype"])
					{
						case 0:
							$rdata = substr($rdata, 12);
							break;
						case 1:
							$rdata = substr($rdata, 16);
							break;
					}
		
					break;
		
				case 0x0b: 
					// Weekly
					if (strlen($rdata) < 16) {
						return $ret;
					}
		
					$data = unpack("Vconst1/Veveryn/Vregen", $rdata);
					$rdata = substr($rdata, 12);

					$ret["everyn"] = $data["everyn"];
					$ret["regen"] = $data["regen"];
					$ret["weekdays"] = 0;

					if ($data["regen"] == 0) {
						$data = unpack("Vweekdays", $rdata);
						$rdata = substr($rdata, 4);

						$ret["weekdays"] = $data["weekdays"];
					}
					break;
		
				case 0x0c: 
					// Monthly
					if (strlen($rdata) < 16) {
						return $ret;
					}
		
					$data = unpack("Vconst1/Veveryn/Vregen/Vmonthday", $rdata);

					$ret["everyn"] = $data["everyn"];
					$ret["regen"] = $data["regen"];
		
					if ($ret["subtype"] == 3) {
						$ret["weekdays"] = $data["monthday"];
					} else {
						$ret["monthday"] = $data["monthday"];
					}
		
					$rdata = substr($rdata, 16);
		
					if ($ret["subtype"] == 3) {
						$data = unpack("Vnday", $rdata);
						$ret["nday"] = $data["nday"];
						$rdata = substr($rdata, 4);
					} 
					break;
		
				case 0x0d: 
					// Yearly
					if (strlen($rdata) < 16)
						return $ret;
		
					$data = unpack("Vmonth/Veveryn/Vregen/Vmonthday", $rdata);
		
					$ret["month"] = $data["month"];
					$ret["everyn"] = $data["everyn"];
					$ret["regen"] = $data["regen"];
					
					if ($ret["subtype"] == 3) {
						$ret["weekdays"] = $data["monthday"];
					} else {
						$ret["monthday"] = $data["monthday"];
					}
					
					$rdata = substr($rdata, 16);
		
					if ($ret["subtype"] == 3) {
						$data = unpack("Vnday", $rdata);
						$ret["nday"] = $data["nday"];
						$rdata = substr($rdata, 4);
					} 
					break;
			} 
		
			if (strlen($rdata) < 16) {
				return $ret;
			}
			
			$data = unpack("Cterm/C3const1/Vnumoccur/Vconst2/Vnumexcept", $rdata);
		
			$rdata = substr($rdata, 16);
		
			$ret["term"] = $data["term"];
			$ret["numoccur"] = $data["numoccur"];
			$ret["numexcept"] = $data["numexcept"]; 
		
			// exc_base_dates are *all* the base dates that have been either deleted or modified
			$exc_base_dates = array();
			for($i = 0; $i < $ret["numexcept"]; $i++)
			{
                if (strlen($rdata) < 4) {
					// We shouldn't arrive here, because that implies
					// numexcept does not match the amount of data
					// which is available for the exceptions.
					return $ret;
				}
				$data = unpack("Vbasedate", $rdata);
				$rdata = substr($rdata, 4);
				$exc_base_dates[] = $this->recurDataToUnixData($data["basedate"]);
			}

			if (strlen($rdata) < 4) {
				return $ret;
			}

			$data = unpack("Vnumexceptmod", $rdata);
			$rdata = substr($rdata, 4);
			
			$ret["numexceptmod"] = $data["numexceptmod"];
		
			// exc_changed are the base dates of *modified* occurrences. exactly what is modified
			// is in the attachments *and* in the data further down this function.
			$exc_changed = array();
			for($i = 0; $i < $ret["numexceptmod"]; $i++)
			{
                if (strlen($rdata) < 4) {
					// We shouldn't arrive here, because that implies
					// numexceptmod does not match the amount of data
					// which is available for the exceptions.
					return $ret;
				}
				$data = unpack("Vstartdate", $rdata);
				$rdata = substr($rdata, 4);
				$exc_changed[] = $this->recurDataToUnixData($data["startdate"]);
			}
		
			if (strlen($rdata) < 8) {
				return $ret;
			}

			$data = unpack("Vstart/Vend", $rdata);
			$rdata = substr($rdata, 8);

			$ret["start"] = $this->recurDataToUnixData($data["start"]);
			$ret["end"] = $this->recurDataToUnixData($data["end"]);

			// this is where task recurrence stop
			if (strlen($rdata) < 16) {
				return $ret;
			}
			
			$data = unpack("Vreaderversion/Vwriterversion/Vstartmin/Vendmin", $rdata);
			$rdata = substr($rdata, 16); 

			$ret["startocc"] = $data["startmin"];
			$ret["endocc"] = $data["endmin"];
			$readerversion = $data["readerversion"];
			$writerversion = $data["writerversion"];

			$data = unpack("vnumber", $rdata);
			$rdata = substr($rdata, 2);

			$nexceptions = $data["number"];
			$exc_changed_details = array();

			// Parse n modified exceptions
			for($i=0;$i<$nexceptions;$i++) 
			{
				$item = array();

				// Get exception startdate, enddate and basedate (the date at which the occurrence would have started)
				$data = unpack("Vstartdate/Venddate/Vbasedate", $rdata);
				$rdata = substr($rdata, 12);

				// Convert recurtimestamp to unix timestamp
				$startdate = $this->recurDataToUnixData($data["startdate"]);
				$enddate = $this->recurDataToUnixData($data["enddate"]);
				$basedate = $this->recurDataToUnixData($data["basedate"]);

				// Set the right properties
				$item["basedate"] = $this->dayStartOf($basedate);
				$item["start"] = $startdate;
				$item["end"] = $enddate;

				$data = unpack("vbitmask", $rdata);
				$rdata = substr($rdata, 2);
				$item["bitmask"] = $data["bitmask"]; // save bitmask for extended exceptions

				// Bitmask to verify what properties are changed
				$bitmask = $data["bitmask"];

				// ARO_SUBJECT: 0x0001
				// Look for field: SubjectLength (2b), SubjectLength2 (2b) and Subject
				if(($bitmask &(1 << 0))) {
					$data = unpack("vnull_length/vlength", $rdata);
					$rdata = substr($rdata, 4);

					$length = $data["length"];
					$item["subject"] = ""; // Normalized subject
					for($j = 0; $j < $length && strlen($rdata); $j++)
					{
						$data = unpack("Cchar", $rdata);
						$rdata = substr($rdata, 1);

						$item["subject"] .= chr($data["char"]);
					}
				}

				// ARO_MEETINGTYPE: 0x0002
				if(($bitmask &(1 << 1))) {
					$rdata = substr($rdata, 4);
					// Attendees modified: no data here (only in attachment)
				}

				// ARO_REMINDERDELTA: 0x0004
				// Look for field: ReminderDelta (4b)
				if(($bitmask &(1 << 2))) {
					$data = unpack("Vremind_before", $rdata);
					$rdata = substr($rdata, 4);

					$item["remind_before"] = $data["remind_before"];
				}

				// ARO_REMINDER: 0x0008
				// Look field: ReminderSet (4b)
				if(($bitmask &(1 << 3))) {
					$data = unpack("Vreminder_set", $rdata);
					$rdata = substr($rdata, 4);

					$item["reminder_set"] = $data["reminder_set"];
				}

				// ARO_LOCATION: 0x0010
				// Look for fields: LocationLength (2b), LocationLength2 (2b) and Location
				// Similar to ARO_SUBJECT above.
				if(($bitmask &(1 << 4))) {
					$data = unpack("vnull_length/vlength", $rdata);
					$rdata = substr($rdata, 4);

					$item["location"] = "";

					$length = $data["length"];
					$data = substr($rdata, 0, $length);
					$rdata = substr($rdata, $length);

					$item["location"] .= $data;
				}

				// ARO_BUSYSTATUS: 0x0020
				// Look for field: BusyStatus (4b)
				if(($bitmask &(1 << 5))) {
					$data = unpack("Vbusystatus", $rdata);
					$rdata = substr($rdata, 4);

					$item["busystatus"] = $data["busystatus"];
				}

				// ARO_ATTACHMENT: 0x0040
				if(($bitmask &(1 << 6))) {
					// no data: RESERVED
					$rdata = substr($rdata, 4);
				}

				// ARO_SUBTYPE: 0x0080
				// Look for field: SubType (4b). Determines whether it is an allday event.
				if(($bitmask &(1 << 7))) {
					$data = unpack("Vallday", $rdata);
					$rdata = substr($rdata, 4);

					$item["alldayevent"] = $data["allday"];
				}

				// ARO_APPTCOLOR: 0x0100
				// Look for field: AppointmentColor (4b)
				if(($bitmask &(1 << 8))) {
					$data = unpack("Vlabel", $rdata);
					$rdata = substr($rdata, 4);

					$item["label"] = $data["label"];
				}

				// ARO_EXCEPTIONAL_BODY: 0x0200
				if(($bitmask &(1 << 9))) {
					// Notes or Attachments modified: no data here (only in attachment)
				}

				array_push($exc_changed_details, $item);
			}

			/**
			 * We now have $exc_changed, $exc_base_dates and $exc_changed_details
			 * We will ignore $exc_changed, as this information is available in $exc_changed_details
			 * also. If an item is in $exc_base_dates and NOT in $exc_changed_details, then the item
			 * has been deleted.
			 */

			// Find deleted occurrences
			$deleted_occurences = array();
			
			foreach($exc_base_dates as $base_date) {
				$found = false;
				
				foreach($exc_changed_details as $details) {
					if($details["basedate"] == $base_date) {
						$found = true;
						break;
					}
				}
				if(! $found) {
					// item was not in exc_changed_details, so it must be deleted
					$deleted_occurences[] = $base_date;
				}
			}

			$ret["deleted_occurences"] = $deleted_occurences;
			$ret["changed_occurences"] = $exc_changed_details;

			// enough data for normal exception (no extended data)
			if (strlen($rdata) < 16) {
				return $ret;
			}

			$data = unpack("Vreservedsize", $rdata);
			$rdata = substr($rdata, 4 + $data["reservedsize"]);

			for($i=0;$i<$nexceptions;$i++) 
			{
				// subject and location in ucs-2 to utf-8
				if ($writerversion >= 0x3009) {
					$data = unpack("Vsize/Vvalue", $rdata); // size includes sizeof(value)==4
					$rdata = substr($rdata, 4 + $data["size"]);
				}

				$data = unpack("Vreservedsize", $rdata);
				$rdata = substr($rdata, 4 + $data["reservedsize"]);

				// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
				if ($exc_changed_details[$i]["bitmask"] & 0x11) {
					$data = unpack("Vstart/Vend/Vorig", $rdata);
					$rdata = substr($rdata, 4 * 3);

					$exc_changed_details[$i]["ex_start_datetime"] = $data["start"];
					$exc_changed_details[$i]["ex_end_datetime"] = $data["end"];
					$exc_changed_details[$i]["ex_orig_date"] = $data["orig"];
				}

				// ARO_SUBJECT
				if ($exc_changed_details[$i]["bitmask"] & 0x01) {
					// decode ucs2 string to utf-8
					$data = unpack("vlength", $rdata);
					$rdata = substr($rdata, 2);
					$length = $data["length"];
					$data = substr($rdata, 0, $length * 2);
					$rdata = substr($rdata, $length * 2);
					$subject = iconv("UCS-2LE", "UTF-8", $data);
					// replace subject with unicode subject
					$exc_changed_details[$i]["subject"] = $subject;
				}

				// ARO_LOCATION
				if ($exc_changed_details[$i]["bitmask"] & 0x10) {
					// decode ucs2 string to utf-8
					$data = unpack("vlength", $rdata);
					$rdata = substr($rdata, 2);
					$length = $data["length"];
					$data = substr($rdata, 0, $length * 2);
					$rdata = substr($rdata, $length * 2);
					$location = iconv("UCS-2LE", "UTF-8", $data);
					// replace subject with unicode subject
					$exc_changed_details[$i]["location"] = $location;
				}

				// ARO_SUBJECT(0x01) | ARO_LOCATION(0x10)
				if ($exc_changed_details[$i]["bitmask"] & 0x11) {
					$data = unpack("Vreservedsize", $rdata);
					$rdata = substr($rdata, 4 + $data["reservedsize"]);
				}
			}

			// update with extended data
			$ret["changed_occurences"] = $exc_changed_details;

			return $ret;
		}

		/**
		 * Saves the recurrence data to the recurrence property
		 * @param array $properties the recurrence data. 
		 * @return string binary string
		 */
		function saveRecurrence()
		{
			// Only save if a message was passed
			if(!isset($this->message))
				return;

			// Abort if no recurrence was set
			if(!isset($this->recur["type"]) && !isset($this->recur["subtype"])) {
				return;
			}

			if(!isset($this->recur["start"]) && !isset($this->recur["end"])) {
				return;
			}

			if(!isset($this->recur["startocc"]) && !isset($this->recur["endocc"])) {
				return;
			}

			$rdata = pack("CCCCCCV", 0x04, 0x30, 0x04, 0x30, intval($this->recur["type"]), 0x20, intval($this->recur["subtype"]));

			$weekstart = 1; //monday
			$forwardcount = 0;
			$restocc = 0;
			$dayofweek = intval(gmdate("w", intval($this->recur["start"]))); //0 (for Sunday) through 6 (for Saturday)

			$term = intval($this->recur["type"]);
			switch($term)
			{
				case 0x0A:
					// Daily
					if(!isset($this->recur["everyn"])) {
						return;
					}

					if($this->recur["subtype"] == 1) {

						// Daily every workday
						$rdata .= pack("VVVV", (6 * 24 * 60), 1, 0, 0x3E);
					} else {
						// Daily every N days (everyN in minutes)

						$everyn =  intval($this->recur["everyn"]) / 1440;

						// Calc first occ
						$firstocc = $this->unixDataToRecurData($this->recur["start"]) % intval($this->recur["everyn"]);

						$rdata .= pack("VVV", $firstocc, intval($this->recur["everyn"]), $this->recur["regen"] ? 1 : 0);
					}
					break;
				case 0x0B:
					// Weekly
					if(!isset($this->recur["everyn"])) {
						return;
					}

					if (!$this->recur["regen"] && !isset($this->recur["weekdays"])) {
						return;
					}

					// No need to calculate startdate if sliding flag was set.
					if (!$this->recur['regen']) {
						// Calculate start date of recurrence

						// Find the first day that matches one of the weekdays selected
						$daycount = 0;
						$dayskip = -1;
						for($j = 0; $j < 7; $j++) {
							if(intval($this->recur["weekdays"]) & (1<<( ($dayofweek+$j)%7)) ) {
								if($dayskip == -1)
									$dayskip = $j;

								$daycount++;
							}
						}

						// $dayskip is the number of days to skip from the startdate until the first occurrence
						// $daycount is the number of days per week that an occurrence occurs

						$weekskip = 0;
						if(($dayofweek < $weekstart && $dayskip > 0) || ($dayofweek+$dayskip) > 6)
							$weekskip = 1;

						// $weekskip is the amount of weeks to skip from the startdate before the first occurence
						// $forwardcount is the maximum number of week occurrences we can go ahead after the first occurrence that
						// is still inside the recurrence. We subtract one to make sure that the last week is never forwarded over
						// (eg when numoccur = 2, and daycount = 1)
						$forwardcount = floor( intval($this->recur["numoccur"] -1 ) / $daycount);

						// $restocc is the number of occurrences left after $forwardcount whole weeks of occurrences, minus one
						// for the occurrence on the first day
						$restocc = intval($this->recur["numoccur"]) - ($forwardcount*$daycount) - 1; 

						// $forwardcount is now the number of weeks we can go forward and still be inside the recurrence
						$forwardcount *= intval($this->recur["everyn"]);
						
						// The real start is start + dayskip + weekskip-1 (since dayskip will already bring us into the next week)
						$this->recur["start"] = intval($this->recur["start"]) + ($dayskip * 24*60*60)+ ($weekskip *(intval($this->recur["everyn"]) - 1) * 7 * 24*60*60);
					}

					// Calc first occ
					$firstocc = ($this->unixDataToRecurData($this->recur["start"]) ) % ( intval($this->recur["everyn"]) * 7 * 24 * 60);

					$firstocc -= (intval(gmdate("w", intval($this->recur["start"]))) - 1) * 24 * 60;

					if ($this->recur["regen"])
						$rdata .= pack("VVV", $firstocc, intval($this->recur["everyn"]), 1);
					else
						$rdata .= pack("VVVV", $firstocc, intval($this->recur["everyn"]), 0, intval($this->recur["weekdays"]));
					break;
				case 0x0C:
					// Monthly
				case 0x0D:
					// Yearly
					if(!isset($this->recur["everyn"])) {
						return;
					}
					if($term == 0x0D /*yearly*/ && !isset($this->recur["month"])) {
						return;
					}
					
					if($term == 0x0C /*monthly*/) {
						$everyn = intval($this->recur["everyn"]);
					}else {
						$everyn = $this->recur["regen"] ? intval($this->recur["everyn"]) * 12 : 12;
					}
					
					// Get montday/month/year of original start
					$curmonthday = gmdate("j", intval($this->recur["start"]) );
					$curyear = gmdate("Y", intval($this->recur["start"]) );
					$curmonth = gmdate("n", intval($this->recur["start"]) );
					
					// $forwardcount is the number of occurrences we can skip and still be inside the recurrence range (minus
					// one to make sure there are always at least one occurrence left)
					$forwardcount = ((intval($this->recur["numoccur"])-1) * $everyn );
					
					// Get month for yearly on D'th day of month M
					if($term == 0x0D /*yearly*/) {
						$selmonth = floor(intval($this->recur["month"]) / (24 * 60 *29)) + 1; // 1=jan, 2=feb, eg
					}
					
					switch(intval($this->recur["subtype"]))
					{
						// on D day of every M month 
						case 2:
							if(!isset($this->recur["monthday"])) {
								return;
							}
							// Recalc startdate
							
							// Set on the right begin day
							
							// Go the beginning of the month
							$this->recur["start"] -= ($curmonthday-1) * 24*60*60;
							// Go the the correct month day
							$this->recur["start"] += (intval($this->recur["monthday"])-1) * 24*60*60;

							// If the previous calculation gave us a start date *before* the original start date, then we need to skip to the next occurrence
							if ( ($term == 0x0C /*monthly*/ && intval($this->recur["monthday"]) < $curmonthday) ||
								($term == 0x0D /*yearly*/ &&( $selmonth < $curmonth || ($selmonth == $curmonth && intval($this->recur["monthday"]) < $curmonthday)) ))
							{
								if($term == 0x0D /*yearly*/)
									$count = ($everyn - ($curmonth - $selmonth)); // Yearly, go to next occurrence in 'everyn' months minus difference in first occurence and original date
								else
									$count = $everyn; // Monthly, go to next occurrence in 'everyn' months

								// Forward by $count months. This is done by getting the number of days in that month and forwarding that many days
								for($i=0; $i < $count; $i++) {
									$this->recur["start"] += $this->getMonthInSeconds($curyear, $curmonth);
									
									if($curmonth == 12) {
										$curyear++;
										$curmonth = 0;
									}
									$curmonth++;
								}
							}
							
							// "start" is now pointing to the first occurrence, except that it will overshoot if the
							// month in which it occurs has less days than specified as the day of the month. So 31st
							// of each month will overshoot in february (29 days). We compensate for that by checking
							// if the day of the month we got is wrong, and then back up to the last day of the previous
							// month.
							if(intval($this->recur["monthday"]) >=28 && intval($this->recur["monthday"]) <= 31 && 
								gmdate("j", intval($this->recur["start"])) < intval($this->recur["monthday"]))
							{
								$this->recur["start"] -= gmdate("j", intval($this->recur["start"])) * 24 * 60 *60;
							}
							
							// "start" is now the first occurrence
							
							if($term == 0x0C /*monthly*/) {
								// Calc first occ
								$monthIndex = ((((12%$everyn) * ((intval(gmdate("Y", $this->recur["start"])) - 1601)%$everyn)) % $everyn) + (intval(gmdate("n", $this->recur["start"])) - 1))%$everyn;

								$firstocc = 0;
								for($i=0; $i < $monthIndex; $i++) {
									$firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), ($i%12)+1) / 60;
								}
								
								$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], intval($this->recur["monthday"]));
							} else{
								// Calc first occ
								$firstocc = 0;
								$monthIndex = intval(gmdate("n", $this->recur["start"]));
								for($i=1; $i < $monthIndex; $i++) {
									$firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), $i) / 60;
								}
								
								$rdata .= pack("VVVV", $firstocc, $everyn, $this->recur["regen"], intval($this->recur["monthday"]));
							}
							break;
						
						case 3:
							// monthly: on Nth weekday of every M month
							// yearly: on Nth weekday of M month
							if(!isset($this->recur["weekdays"]) && !isset($this->recur["nday"])) {
								return;
							}
							
							$weekdays = intval($this->recur["weekdays"]);
							$nday = intval($this->recur["nday"]);
							
							// Calc startdate
							$monthbegindow = intval($this->recur["start"]);
							
							if($nday == 5) {
								// Set date on the last day of the last month
								$monthbegindow += (gmdate("t", $monthbegindow ) - gmdate("j", $monthbegindow )) * 24 * 60 * 60;
							}else {
								// Set on the first day of the month
								$monthbegindow -= ((gmdate("j", $monthbegindow )-1) * 24 * 60 * 60);
							}

							if($term == 0x0D /*yearly*/) {
								// Set on right month
								if($selmonth < $curmonth)
									$tmp = 12 - $curmonth + $selmonth;
								else
									$tmp = ($selmonth - $curmonth);
								
								for($i=0; $i < $tmp; $i++) {
									$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
									
									if($curmonth == 12) {
										$curyear++;
										$curmonth = 0;
									}
									$curmonth++;
								}
								
							}else {
								// Check or you exist in the right month

								for($i = 0; $i < 7; $i++) {
									if($nday == 5 && (1<<( (gmdate("w", $monthbegindow)-$i)%7) ) & $weekdays) {
										$day = gmdate("j", $monthbegindow) - $i;
										break;
									}else if($nday != 5 && (1<<( (gmdate("w", $monthbegindow )+$i)%7) ) & $weekdays) {
										$day = (($nday-1)*7) + ($i+1);
										break;
									}
								}
								
								// Goto the next X month
								if(isset($day) && ($day < gmdate("j", intval($this->recur["start"]))) ) {
									if($nday == 5) {
										$monthbegindow += 24 * 60 * 60;
										if($curmonth == 12) {
											$curyear++;
											$curmonth = 0;
										}
										$curmonth++;
									}
								
									for($i=0; $i < $everyn; $i++) {
										$monthbegindow += $this->getMonthInSeconds($curyear, $curmonth);
										
										if($curmonth == 12) {
											$curyear++;
											$curmonth = 0;
										}
										$curmonth++;
									}
									
									if($nday == 5) {
										$monthbegindow -= 24 * 60 * 60;
									}
								}
							}
								
							//FIXME: weekstart?
							
							$day = 0;
							// Set start on the right day
							for($i = 0; $i < 7; $i++) {
								if($nday == 5 && (1<<( (gmdate("w", $monthbegindow )-$i)%7) ) & $weekdays) {
									$day = $i;
									break;
								}else if($nday != 5 && (1<<( (gmdate("w", $monthbegindow )+$i)%7) ) & $weekdays) {
									$day = ($nday - 1) * 7 + ($i+1);
									break;
								}
							}
							if($nday == 5)
								$monthbegindow -= $day * 24 * 60 *60;
							else
								$monthbegindow += ($day-1) * 24 * 60 *60;

							$firstocc = 0;
							
							if($term == 0x0C /*monthly*/) {
								// Calc first occ
								$monthIndex = ((((12%$everyn) * ((intval(gmdate("Y", $this->recur["start"])) - 1601)%$everyn)) % $everyn) + (intval(gmdate("n", $this->recur["start"])) - 1))%$everyn;
								
								for($i=0; $i < $monthIndex; $i++) {
									$firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), ($i%12)+1) / 60;
								}
								
								$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
							} else {
								// Calc first occ								
								$monthIndex = intval(gmdate("n", $this->recur["start"]));
								
								for($i=1; $i < $monthIndex; $i++) {
									$firstocc+= $this->getMonthInSeconds(1601 + floor($i/12), $i) / 60;
								}
								
								$rdata .= pack("VVVVV", $firstocc, $everyn, 0, $weekdays, $nday);
							}
							break;
					}
					break;
				


			}
			
			if(!isset($this->recur["term"])) {
				return;
			}
			
			// Terminate
			$term = intval($this->recur["term"]);
			$rdata .= pack("CCCC", $term, 0x20, 0x00, 0x00);
			
			switch($term)
			{
				// After the given enddate
				case 0x21:
					$rdata .= pack("V", 10);
					break;
				// After a number of times
				case 0x22:
					if(!isset($this->recur["numoccur"])) {
						return;
					}
					
					$rdata .= pack("V", intval($this->recur["numoccur"]));
					break;
				// Never ends
				case 0x23:
					$rdata .= pack("V", 0);
					break;
			}
			
			// Strange little thing for the recurrence type "every workday"
			if(intval($this->recur["type"]) == 0x0B && intval($this->recur["subtype"]) == 1) {
				$rdata .= pack("V", 1);
			} else { // Other recurrences
				$rdata .= pack("V", 0);
			}
			
			// Exception data

			// Get all exceptions
			$deleted_items = $this->recur["deleted_occurences"];
			$changed_items = $this->recur["changed_occurences"];
			
			// Merge deleted and changed items into one list
			$items = $deleted_items;
			
			foreach($changed_items as $changed_item)
				array_push($items, $changed_item["basedate"]);

			sort($items);
			
			// Add the merged list in to the rdata
			$rdata .= pack("V", count($items));
			foreach($items as $item)
				$rdata .= pack("V", $this->unixDataToRecurData($item));
			
			// Loop through the changed exceptions (not deleted)
			$rdata .= pack("V", count($changed_items));
			$items = array();
			
			foreach($changed_items as $changed_item)
			{
				$items[] = $this->dayStartOf($changed_item["start"]);
			}

			sort($items);

			// Add the changed items list int the rdata
			foreach($items as $item)
				$rdata .= pack("V", $this->unixDataToRecurData($item));
			
			// Set start date
			$rdata .= pack("V", $this->unixDataToRecurData(intval($this->recur["start"])));
		
			// Set enddate
			switch($term)
			{
				// After the given enddate
				case 0x21:
					$rdata .= pack("V", $this->unixDataToRecurData(intval($this->recur["end"])));
					break;
				// After a number of times
				case 0x22:
					// @todo: calculate enddate with intval($this->recur["startocc"]) + intval($this->recur["duration"]) > 24 hour
					$occenddate = intval($this->recur["start"]);
					
					switch(intval($this->recur["type"])) {
						case 0x0A: //daily
														
							if($this->recur["subtype"] == 1) {
								// Daily every workday
								$restocc = intval($this->recur["numoccur"]);
								
								// Get starting weekday
								$nowtime = $this->gmtime($occenddate);
								$j = $nowtime["tm_wday"];
								
								while(1)
								{
									if(($j%7) > 0 && ($j%7)<6 ) {
										$restocc--;
									}
									
									$j++;

									if($restocc <= 0)
										break;

									$occenddate += 24*60*60;
								}
								
							} else {
								// -1 because the first day already counts (from 1-1-1980 to 1-1-1980 is 1 occurrence)
								$occenddate += (intval($this->recur["everyn"]) * 60 * (intval($this->recur["numoccur"]-1)));
							}
							break;
						case 0x0B: //weekly
							// Needed values
							// $forwardcount - number of weeks we can skip forward
							// $restocc - number of remaning occurrences after the week skip
			
							// Add the weeks till the last item
							$occenddate+=($forwardcount*7*24*60*60);
							
							$dayofweek = gmdate("w", $occenddate);
							
							// Loop through the last occurrences until we have had them all
							for($j = 1; $restocc>0; $j++)
							{
								// Jump to the next week (which may be N weeks away) when going over the week boundary
								if((($dayofweek+$j)%7) == $weekstart)
									$occenddate += (intval($this->recur["everyn"])-1) * 7 * 24*60*60;

								// If this is a matching day, once less occurrence to process
								if(intval($this->recur["weekdays"]) & (1<<(($dayofweek+$j)%7)) ) {
									$restocc--;
								}
								
								// Next day
								$occenddate += 24*60*60;
							}
							
							break;
						case 0x0C: //monthly
						case 0x0D: //yearly
								
							$curyear = gmdate("Y", intval($this->recur["start"]) );
							$curmonth = gmdate("n", intval($this->recur["start"]) );
							// $forwardcount = months

							switch(intval($this->recur["subtype"]))
							{
								case 2: // on D day of every M month
									while($forwardcount > 0)
									{
										$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
										
										if($curmonth >=12) {
											$curmonth = 1;
											$curyear++;
										} else { 
											$curmonth++;
										}
										$forwardcount--;
									}
									
									// compensation between 28 and 31
									if(intval($this->recur["monthday"]) >=28 && intval($this->recur["monthday"]) <= 31 && 
										gmdate("j", $occenddate) < intval($this->recur["monthday"]))
									{
										if(gmdate("j", $occenddate) < 28)
											$occenddate -= gmdate("j", $occenddate) * 24 * 60 *60;
										else
											$occenddate += (gmdate("t", $occenddate) - gmdate("j", $occenddate)) * 24 * 60 *60;
									}
									
									
									break;
								case 3: // on Nth weekday of every M month
									$nday = intval($this->recur["nday"]); //1 tot 5
									$weekdays = intval($this->recur["weekdays"]);
									

									while($forwardcount > 0)
									{
										$occenddate += $this->getMonthInSeconds($curyear, $curmonth);
										if($curmonth >=12) {
											$curmonth = 1;
											$curyear++;
										} else {
											$curmonth++;
										}

										$forwardcount--;
									}

									if($nday == 5) {
										// Set date on the last day of the last month
										$occenddate += (gmdate("t", $occenddate ) - gmdate("j", $occenddate )) * 24 * 60 * 60;
									}else {
										// Set date on the first day of the last month
										$occenddate -= (gmdate("j", $occenddate )-1) * 24 * 60 * 60;
									}
									
									for($i = 0; $i < 7; $i++) {
										if( $nday == 5 && (1<<( (gmdate("w", $occenddate)-$i)%7) ) & $weekdays) {
											$occenddate -= $i * 24 * 60 * 60;
											break;
										}else if($nday != 5 && (1<<( (gmdate("w", $occenddate)+$i)%7) ) & $weekdays) {
											$occenddate +=  ($i + (($nday-1) *7)) * 24 * 60 * 60;
											break;
										}
									}

								break; //case 3:
								}

							break;

					}

					if (defined("PHP_INT_MAX") && $occenddate > PHP_INT_MAX)
						$occenddate = PHP_INT_MAX;
					
					$this->recur["end"] = $occenddate;

					$rdata .= pack("V", $this->unixDataToRecurData(intval($this->recur["end"])) );
					break;
				// Never ends
				case 0x23:
				default:
					$this->recur["end"] = 0x7fffffff; // max date -> 2038
					$rdata .= pack("V", 0x5AE980DF);
					break;
			}

			// UTC date
			$utcstart = $this->toGMT($this->tz, intval($this->recur["start"]));
			$utcend = $this->toGMT($this->tz, intval($this->recur["end"]));
			
			//utc date+time
			$utcfirstoccstartdatetime = (isset($this->recur["startocc"])) ? $utcstart + (intval($this->recur["startocc"])*60) : $utcstart;
			$utcfirstoccenddatetime = (isset($this->recur["endocc"])) ? $utcstart + (intval($this->recur["endocc"]) * 60) : $utcstart;
			
			// update reminder time
			mapi_setprops($this->message, Array($this->proptags["reminder_time"] => $utcfirstoccstartdatetime ));

			// update first occurrence date
			mapi_setprops($this->message, Array($this->proptags["startdate"] => $utcfirstoccstartdatetime ));
			mapi_setprops($this->message, Array($this->proptags["duedate"] => $utcfirstoccenddatetime ));
			mapi_setprops($this->message, Array($this->proptags["commonstart"] => $utcfirstoccstartdatetime ));
			mapi_setprops($this->message, Array($this->proptags["commonend"] => $utcfirstoccenddatetime ));

			// Set Outlook properties, if it is an appointment
			if (isset($this->recur["message_class"]) && $this->recur["message_class"] == "IPM.Appointment") {
				// update real begin and real end date
				mapi_setprops($this->message, Array($this->proptags["startdate_recurring"] => $utcstart));
				mapi_setprops($this->message, Array($this->proptags["enddate_recurring"] => $utcend));

				// recurrencetype
				// Strange enough is the property recurrencetype, (type-0x9) and not the CDO recurrencetype
				mapi_setprops($this->message, Array($this->proptags["recurrencetype"] => intval($this->recur["type"]) - 0x9));

				// set named prop 'side_effects' to 369, needed for Outlook to ask for single or total recurrence when deleting
				mapi_setprops($this->message, Array($this->proptags["side_effects"] => 369));
			} else {
				mapi_setprops($this->message, Array($this->proptags["side_effects"] => 3441));
			}

			// FlagDueBy is datetime of the first reminder occurrence. Outlook gives on this time a reminder popup dialog
			$reminderprops = mapi_getprops($this->message, array($this->proptags["reminder_minutes"], $this->proptags["flagdueby"]) );
			if(isset($reminderprops[$this->proptags["reminder_minutes"]]) ) {
				/**
				 * TODO: $utcfirstoccstartdatetime must the first upcomming occurrence, so you need the current 
-				 * client time. When you create a recurrent before the current date, the FlagDueBy must be the 
-				 * first upcomming occurrence after the current date.
				 * 
				 * Get the next occurrence after the current flagdueby i.e. reminder and set that occurrence's
				 * reminder time to the flagdueby property.
				 */
				$items = $this->getItems($reminderprops[$this->proptags["flagdueby"]], 0x7ff00000, 1, true);
				$tempnextreminder = $items[0][$this->proptags["startdate"]] - ( $items[0][$this->proptags["reminder_minutes"]] * 60 );
				mapi_setprops($this->message, Array($this->proptags["flagdueby"] => $tempnextreminder ));
			}
			
			// Default data
			// Second item (0x08) indicates the Outlook version (see documentation at the bottom of this file for more information) 
			$rdata .= pack("VCCCC", 0x00003006, 0x08, 0x30, 0x00, 0x00);
		
			if(isset($this->recur["startocc"]) && isset($this->recur["endocc"])) {
				// Set start and endtime in minutes
				$rdata .= pack("VV", intval($this->recur["startocc"]), intval($this->recur["endocc"]));
			}

			// Detailed exception data
			
			$changed_items = $this->recur["changed_occurences"];
				
			$rdata .= pack("v", count($changed_items));
			
			foreach($changed_items as $changed_item)
			{
				// Set start and end time of exception
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
				$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));

				//Bitmask
				$bitmask = 0;

				// Check for changed strings
				if(isset($changed_item["subject"]))	{
					$bitmask |= 1 << 0;
				}

				if(isset($changed_item["remind_before"])) {
					$bitmask |= 1 << 2;
				}

				if(isset($changed_item["reminder_set"])) {
					$bitmask |= 1 << 3;
				}

				if(isset($changed_item["location"])) {
					$bitmask |= 1 << 4;
				}

				if(isset($changed_item["busystatus"])) {
					$bitmask |= 1 << 5;
				}

				if(isset($changed_item["alldayevent"]))	{
					$bitmask |= 1 << 7;
				}

				if(isset($changed_item["label"])) {
					$bitmask |= 1 << 8;
				}

				$rdata .= pack("v", $bitmask);

				// Set "subject"
				if(isset($changed_item["subject"]))	{
					// convert utf-8 to non-unicode blob string (us-ascii?)
					$subject = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["subject"]);
					$length = strlen($subject);
					$rdata .= pack("vv", $length + 1, $length);
					$rdata .= pack("a".$length, $subject);
				}

				if(isset($changed_item["remind_before"])) {
					$rdata .= pack("V", $changed_item["remind_before"]);
				}

				if(isset($changed_item["reminder_set"])) {
					$rdata .= pack("V", $changed_item["reminder_set"]);
				}

				if(isset($changed_item["location"])) {
					$location = iconv("UTF-8", "windows-1252//TRANSLIT", $changed_item["location"]);
					$length = strlen($location);
					$rdata .= pack("vv", $length + 1, $length);
					$rdata .= pack("a".$length, $location);
				}

				if(isset($changed_item["busystatus"])) {
					$rdata .= pack("V", $changed_item["busystatus"]);
				}

				if(isset($changed_item["alldayevent"]))	{
					$rdata .= pack("V", $changed_item["alldayevent"]);
				}

				if(isset($changed_item["label"])) {
					$rdata .= pack("V", $changed_item["label"]);
				}
			}

			$rdata .= pack("V", 0);

			// write extended data
			foreach($changed_items as $changed_item)
			{
				$rdata .= pack("V", 0);
				if(isset($changed_item["subject"]) || isset($changed_item["location"])) {
					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["start"]));
					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["end"]));
					$rdata .= pack("V", $this->unixDataToRecurData($changed_item["basedate"]));
				}

				if(isset($changed_item["subject"])) {
					$subject = iconv("UTF-8", "UCS-2LE", $changed_item["subject"]);
					$length = iconv_strlen($subject, "UCS-2LE");
					$rdata .= pack("v", $length);
					$rdata .= pack("a".$length*2, $subject);
				}

				if(isset($changed_item["location"])) {
					$location = iconv("UTF-8", "UCS-2LE", $changed_item["location"]);
					$length = iconv_strlen($location, "UCS-2LE");
					$rdata .= pack("v", $length);
					$rdata .= pack("a".$length*2, $location);
				}

				if(isset($changed_item["subject"]) || isset($changed_item["location"])) {
					$rdata .= pack("V", 0);
				}
			}

			$rdata .= pack("V", 0);

			// Set props
			mapi_setprops($this->message, Array($this->proptags["recurring_data"] => $rdata, $this->proptags["recurring"] => true));
			if(isset($this->tz) && $this->tz){
				$timezone = "GMT";
				if ($this->tz["timezone"]!=0){
					// Create user readable timezone information
					$timezone = sprintf("(GMT %s%02d:%02d)",	(-$this->tz["timezone"]>0 ? "+" : "-"), 
															abs($this->tz["timezone"]/60), 
															abs($this->tz["timezone"]%60));
				}
				mapi_setprops($this->message, Array($this->proptags["timezone_data"] => $this->getTimezoneData($this->tz),
													$this->proptags["timezone"] => $timezone));
			}
		}



















				/**
		* Function which converts a recurrence date timestamp to an unix date timestamp.
		* @author Steve Hardy
		* @param Int $rdate the date which will be converted
		* @return Int the converted date
		*/ 
		function recurDataToUnixData($rdate)
		{
			return ($rdate - 194074560) * 60 ;
		}

		/**
		* Function which converts an unix date timestamp to recurrence date timestamp.
		* @author Johnny Biemans
		* @param Date $date the date which will be converted
		* @return Int the converted date in minutes
		*/ 
		function unixDataToRecurData($date)
		{
			return ($date / 60) + 194074560;
		}

		/**
		* gmtime() doesn't exist in standard PHP, so we have to implement it ourselves
		* @author Steve Hardy
		*/
		function GetTZOffset($ts)
		{
			$Offset = date("O", $ts);

			$Parity = $Offset < 0 ? -1 : 1;
			$Offset = $Parity * $Offset;
			$Offset = ($Offset - ($Offset % 100)) / 100 * 60 + $Offset % 100;

			return $Parity * $Offset;
		}

		/**
		* gmtime() doesn't exist in standard PHP, so we have to implement it ourselves
		* @author Steve Hardy
		* @param Date $time
		* @return Date GMT Time
		*/
		function gmtime($time)
		{
			$TZOffset = $this->GetTZOffset($time);

			$t_time = $time - $TZOffset * 60; #Counter adjust for localtime()
			$t_arr = localtime($t_time, 1);

			return $t_arr;
		}

		function isLeapYear($year) {
			return ( $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0) );
		}

		function getMonthInSeconds($year, $month)
		{
			if( in_array($month, array(1,3,5,7,8,10,12) ) ) {
				$day = 31;
			} else if( in_array($month, array(4,6,9,11) ) ) {
				$day = 30;
			} else {
				$day = 28;
				if( $this->isLeapYear($year) == 1 )
					$day++;
			}
			return $day * 24 * 60 * 60;
		}

		/**
		 * Function to get a date by Year Nr, Month Nr, Week Nr, Day Nr, and hour
		 * @param int $year
		 * @param int $month
		 * @param int $week
		 * @param int $day
		 * @param int $hour
		 * @return returns the timestamp of the given date, timezone-independant
		 */
		function getDateByYearMonthWeekDayHour($year, $month, $week, $day, $hour)
		{
			// get first day of month
			$date = gmmktime(0,0,0,$month,0,$year + 1900);

			// get wday info
			$gmdate = $this->gmtime($date);

			$date -= $gmdate["tm_wday"] * 24 * 60 * 60; // back up to start of week

			$date += $week * 7 * 24 * 60 * 60; // go to correct week nr
			$date += $day * 24 * 60 * 60;
			$date += $hour * 60 * 60;

			$gmdate = $this->gmtime($date);

			// if we are in the next month, then back up a week, because week '5' means
			// 'last week of month'

			if($gmdate["tm_mon"]+1 != $month)
				$date -= 7 * 24 * 60 * 60;

			return $date; 
		}

		/**
		 * getTimezone gives the timezone offset (in minutes) of the given
		 * local date/time according to the given TZ info
		 */
		function getTimezone($tz, $date)
		{
			// No timezone -> GMT (+0)
			if(!isset($tz["timezone"]))
				return 0;

			$dst = false;
			$gmdate = $this->gmtime($date);

			$dststart = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dststartmonth"], $tz["dststartweek"], 0, $tz["dststarthour"]);
			$dstend = $this->getDateByYearMonthWeekDayHour($gmdate["tm_year"], $tz["dstendmonth"], $tz["dstendweek"], 0, $tz["dstendhour"]);

			if($dststart < $dstend) {
				// Northern hemisphere, eg DST is during Mar-Oct
				if($date > $dststart && $date < $dstend) {
					$dst = true;
				}
			} else {
				// Southern hemisphere, eg DST is during Oct-Mar
				if($date < $dstend || $date > $dststart) {
					$dst = true;
				}
			}

			if($dst) {
				return $tz["timezone"] + $tz["timezonedst"];
			} else {
				return $tz["timezone"];
			}
		}

		/**
		 * getWeekNr() returns the week nr of the month (ie first week of february is 1)
		 */
		function getWeekNr($date)
		{
			$gmdate = gmtime($date);
			$gmdate["tm_mday"] = 0;
			return strftime("%W", $date) - strftime("%W", gmmktime($gmdate)) + 1;
		}

		/** 
		 * parseTimezone parses the timezone as specified in named property 0x8233
		 * in Outlook calendar messages. Returns the timezone in minutes negative
		 * offset (GMT +2:00 -> -120)
		 */
		function parseTimezone($data)
		{
			if(strlen($data) < 48)
				return;

			$tz = unpack("ltimezone/lunk/ltimezonedst/lunk/ldstendmonth/vdstendweek/vdstendhour/lunk/lunk/vunk/ldststartmonth/vdststartweek/vdststarthour/lunk/vunk", $data);
			return $tz;
		}

		function getTimezoneData($tz)
		{
			$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);

			return $data;
		}

		/**
		 * createTimezone creates the timezone as specified in the named property 0x8233
		 * see also parseTimezone()
		 * $tz is an array with the timezone data
		 */
		function createTimezone($tz)
		{
			$data = pack("lxxxxlxxxxlvvxxxxxxxxxxlvvxxxxxx", 
						$tz["timezone"],
						array_key_exists("timezonedst",$tz)?$tz["timezonedst"]:0,
						array_key_exists("dstendmonth",$tz)?$tz["dstendmonth"]:0,
						array_key_exists("dstendweek",$tz)?$tz["dstendweek"]:0,
						array_key_exists("dstendhour",$tz)?$tz["dstendhour"]:0,
						array_key_exists("dststartmonth",$tz)?$tz["dststartmonth"]:0,
						array_key_exists("dststartweek",$tz)?$tz["dststartweek"]:0,
						array_key_exists("dststarthour",$tz)?$tz["dststarthour"]:0
					);

			return $data;
		}

		/**
		 * toGMT returns a timestamp in GMT time for the time and timezone given
		 */
		function toGMT($tz, $date) {
			if(!isset($tz['timezone']))
				return $date;
			$offset = $this->getTimezone($tz, $date);

			return $date + $offset * 60;
		}

		/**
		 * fromGMT returns a timestamp in the local timezone given from the GMT time given
		 */
		function fromGMT($tz, $date) {
			$offset = $this->getTimezone($tz, $date);

			return $date - $offset * 60;
		}

		/**
		 * Function to get timestamp of the beginning of the day of the timestamp given
		 * @param date $date
		 * @return date timestamp referring to same day but at 00:00:00
		 */
		function dayStartOf($date)
		{
			$time1 = $this->gmtime($date);

			return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, $time1["tm_mday"], $time1["tm_year"] + 1900);
		}
		
		/**
		 * Function to get timestamp of the beginning of the month of the timestamp given
		 * @param date $date
		 * @return date Timestamp referring to same month but on the first day, and at 00:00:00
		 */
		function monthStartOf($date)
		{
			$time1 = $this->gmtime($date);

			return gmmktime(0, 0, 0, $time1["tm_mon"] + 1, 1, $time1["tm_year"] + 1900);
		}
		
		/**
		 * Function to get timestamp of the beginning of the year of the timestamp given
		 * @param date $date
		 * @return date Timestamp referring to the same year but on Jan 01, at 00:00:00
		 */
		function yearStartOf($date)
		{
			$time1 = $this->gmtime($date);

			return gmmktime(0, 0, 0, 1, 1, $time1["tm_year"] + 1900);
		}


		/**
		 * Function which returns the items in a given interval.
		 * @param string $entryid the entryid of the message
		 * @param array $props the properties of the message
		 * @param date $start start time of the interval (GMT)
		 * @param date $end end time of the interval (GMT)
		 */
		function getItems($start, $end, $limit = 0, $remindersonly = false)
		{
			$items = array();

			if(isset($this->recur)) {

				// Optimization: remindersonly and default reminder is off; since only exceptions with reminder set will match, just look which
				// exceptions are in range and have a reminder set
				if($remindersonly && (!isset($this->messageprops[$this->proptags["reminder"]]) || $this->messageprops[$this->proptags["reminder"]] == false)) {
					// Sort exceptions by start time
					uasort($this->recur["changed_occurences"], array($this, "sortExceptionStart"));

					// Loop through all changed exceptions
					foreach($this->recur["changed_occurences"] as $exception) {
						// Check reminder set
						if(!isset($exception["reminder"]) || $exception["reminder"] == false)
							continue;

						// Convert to GMT
						$occstart = $this->toGMT($tz, $exception["start"]);
						$occend = $this->toGMT($tz, $exception["end"]);

						// Check range criterium
						if($occstart > $end || $occend < $start)
							continue;

						// OK, add to items.
						array_push($items, $this->getExceptionProperties($exception));
						if($limit && (count($items) == $limit))
							break;
					}

					uasort($items, array($this, "sortStarttime"));

					return $items;
				}

				// From here on, the dates of the occurrences are calculated in local time, so the days we're looking
				// at are calculated from the local time dates of $start and $end

				if ($this->recur['regen'] && isset($this->action['datecompleted'])) {
					$daystart = $this->dayStartOf($this->action['datecompleted']);
				} else {
					$daystart = $this->dayStartOf($this->recur["start"]); // start on first day of occurrence
				}

				// Calculate the last day on which we want to be looking at a recurrence; this is either the end of the view
				// or the end of the recurrence, whichever comes first
				if($end > $this->toGMT($this->tz, $this->recur["end"])) {
					$rangeend = $this->toGMT($this->tz, $this->recur["end"]);
				} else {
					$rangeend = $end;
				}

				$dayend = $this->dayStartOf($this->fromGMT($this->tz, $rangeend));

				// Loop through the entire recurrence range of dates, and check for each occurrence whether it is in the view range.

				switch($this->recur["type"])
				{
				case 10:
					// Daily
					if($this->recur["everyn"] <= 0)
						$this->recur["everyn"] = 1440;
						
					if($this->recur["subtype"] == 0) {
						// Every Nth day
						for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * $this->recur["everyn"]) {
							$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
						}
					} else {
						// Every workday
						for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += 60 * 1440)
						{
							$nowtime = $this->gmtime($now);
							if ($nowtime["tm_wday"] > 0 && $nowtime["tm_wday"] < 6) { // only add items in the given timespace
								$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
							}
						}
					}
					break;
				case 11:
					// Weekly
					if($this->recur["everyn"] <= 0)
						$this->recur["everyn"] = 1;

					// If sliding flag is set then move to 'n' weeks
					if ($this->recur['regen']) $daystart += (60 * 60 * 24 * 7 * $this->recur["everyn"]);

					for($now = $daystart; $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += (60 * 60 * 24 * 7 * $this->recur["everyn"]))
					{
						if ($this->recur['regen']) {
							$this->processOccurrenceItem($items, $start, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
						} else {
							// Loop through the whole following week to the first occurrence of the week, add each day that is specified
							for($wday = 0; $wday < 7; $wday++)
							{
								$daynow = $now + $wday * 60 * 60 * 24;
								//checks weather the next coming day in recurring pattern is less than or equal to end day of the recurring item
								if ($daynow <= $dayend){
									$nowtime = $this->gmtime($daynow); // Get the weekday of the current day
									if(($this->recur["weekdays"] &(1 << $nowtime["tm_wday"]))) { // Selected ?
										$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
									}
								}
							}
						}
					}
					break;
				case 12:
					// Monthly
					if($this->recur["everyn"] <= 0)
						$this->recur["everyn"] = 1;

					// Loop through all months from start to end of occurrence, starting at beginning of first month
					for($now = $this->monthStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 )
					{
						if(isset($this->recur["monthday"]) &&($this->recur['monthday'] != "undefined") && !$this->recur['regen']) { // Day M of every N months
							$difference = 1;
							if ($this->daysInMonth($now, $this->recur["everyn"]) < $this->recur["monthday"]) {
								$difference = $this->recur["monthday"] - $this->daysInMonth($now, $this->recur["everyn"]) + 1;
							}
							$daynow = $now + (($this->recur["monthday"] - $difference) * 24 * 60 * 60);
							//checks weather the next coming day in recurrence pattern is less than or equal to end day of the recurring item 
							if ($daynow <= $dayend){
								$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
							}
						}
						else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] of every N months
							// If nday is not set to the last day in the month
							if ($this->recur["nday"] < 5) {
								// keep the track of no. of time correct selection pattern(like 2nd weekday, 4th fiday, etc.)is matched 
								$ndaycounter = 0;
								// Find matching weekday in this month
								for($day = 0; $day < $this->daysInMonth($now, 1); $day++)
								{
									$daynow = $now + $day * 60 * 60 * 24;
									$nowtime = $this->gmtime($daynow); // Get the weekday of the current day

									if($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
										$ndaycounter ++;
									}
									// check the selected pattern is same as asked Nth weekday,If so set the firstday
									if($this->recur["nday"] == $ndaycounter){
										$firstday = $day;
										break;
									}
								}
								// $firstday is the day of the month on which the asked pattern of nth weekday matches
								$daynow = $now + $firstday * 60 * 60 * 24;
							}else{
								// Find last day in the month ($now is the firstday of the month)
								$NumDaysInMonth =  $this->daysInMonth($now, 1);
								$daynow = $now + (($NumDaysInMonth-1) * 24*60*60);

								$nowtime = $this->gmtime($daynow);
								while (($this->recur["weekdays"] & (1 << $nowtime["tm_wday"]))==0){
									$daynow -= 86400;
									$nowtime = $this->gmtime($daynow);
								}
							}

							/** 
							 * 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.
							 */
							if ($daynow <= $dayend && $daynow >= $daystart){
								$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz , $remindersonly);
							}
						} else if ($this->recur['regen']) {
							$next_month_start = $now + ($this->daysInMonth($now, 1) * 24 * 60 * 60);
							$now = $daystart +($this->daysInMonth($next_month_start, $this->recur['everyn']) * 24 * 60 * 60);

							if ($now <= $dayend) {
								$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
							}
						}
					}
					break;
				case 13:
					// Yearly
					if($this->recur["everyn"] <= 0)
						$this->recur["everyn"] = 12;

					for($now = $this->yearStartOf($daystart); $now <= $dayend && ($limit == 0 || count($items) < $limit); $now += $this->daysInMonth($now, $this->recur["everyn"]) * 24 * 60 * 60 )
					{
						if(isset($this->recur["monthday"]) && !$this->recur['regen']) { // same as monthly, but in a specific month
							// recur["month"] is in minutes since the beginning of the year
							$month = $this->monthOfYear($this->recur["month"]); // $month is now month of year [0..11]
							$monthday = $this->recur["monthday"]; // $monthday is day of the month [1..31]
							$monthstart = $now + $this->daysInMonth($now, $month) * 24 * 60 * 60; // $monthstart is the timestamp of the beginning of the month
							if($monthday > $this->daysInMonth($monthstart, 1))
								$monthday = $this->daysInMonth($monthstart, 1);	// Cap $monthday on month length (eg 28 feb instead of 29 feb)
							$daynow = $monthstart + ($monthday-1) * 24 * 60 * 60;
							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
						}
						else if(isset($this->recur["nday"]) && isset($this->recur["weekdays"])) { // Nth [weekday] in month X of every N years

							// Go the correct month
							$monthnow = $now + $this->daysInMonth($now, $this->monthOfYear($this->recur["month"])) * 24 * 60 * 60;

							// Find first matching weekday in this month
							for($wday = 0; $wday < 7; $wday++)
							{
								$daynow = $monthnow + $wday * 60 * 60 * 24;
								$nowtime = $this->gmtime($daynow); // Get the weekday of the current day

								if($this->recur["weekdays"] & (1 << $nowtime["tm_wday"])) { // Selected ?
									$firstday = $wday;
									break;
								}
							}

							// Same as above (monthly)
							$daynow = $monthnow + ($firstday + ($this->recur["nday"]-1)*7) * 60 * 60 * 24;

							while($this->monthStartOf($daynow) != $this->monthStartOf($monthnow)) {
								$daynow -= 7 * 60 * 60 * 24;
							}

							$this->processOccurrenceItem($items, $start, $end, $daynow, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
						} else if ($this->recur['regen']) {
							$year_starttime = $this->gmtime($now);
							$is_next_leapyear = $this->isLeapYear($year_starttime['tm_year'] + 1900 + 1);	// +1 next year
							$now = $daystart + ($is_next_leapyear ? 31622400 /* Leap year in seconds */ : 31536000 /*year in seconds*/);

							if ($now <= $dayend) {
								$this->processOccurrenceItem($items, $daystart, $end, $now, $this->recur["startocc"], $this->recur["endocc"], $this->tz, $remindersonly);
							}
						}
					}
				}
				//to get all exception items
				if (count($this->recur['changed_occurences']) > 0)
					$this->processExceptionItems($items, $start, $end);
			}

			// sort items on starttime
			usort($items, array($this, "sortStarttime"));

			// Return the MAPI-compatible list of items for this object
			return $items;
		}

		function sortStarttime($a, $b)
		{
			$aTime = $a[$this->proptags["startdate"]];
			$bTime = $b[$this->proptags["startdate"]];

			return $aTime==$bTime?0:($aTime>$bTime?1:-1);
		}

		/**
		 * daysInMonth
		 * 
		 * Returns the number of days in the upcoming number of months. If you specify 1 month as 
		 * $months it will give you the number of days in the month of $date. If you specify more it
		 * will also count the days in the upcomming months and add that to the number of days. So 
		 * if you have a date in march and you specify $months as 2 it will return 61.
		 * @param Integer $date Specified date as timestamp from which you want to know the number 
		 * of days in the month.
		 * @param Integer $months Number of months you want to know the number of days in.
		 * @returns Integer Number of days in the specified amount of months.
		 */
		function daysInMonth($date, $months) {
		    $days = 0;
		    
		    for($i=0;$i<$months;$i++) {
		        $days += date("t", $date + $days * 24 * 60 * 60);
		    }
		    
		    return $days;
		}
		
		// Converts MAPI-style 'minutes' into the month of the year [0..11]
		function monthOfYear($minutes) {
		    $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
		    
		    $d += $minutes*60;
		    
		    $dtime = $this->gmtime($d);
		    
		    return $dtime["tm_mon"];
		}

		function sortExceptionStart($a, $b)
		{
		    return $a["start"] == $b["start"] ? 0 : ($a["start"]  > $b["start"] ? 1 : -1 );
		}
	}
?>
