<?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."
 * 
 * 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
	/**
	 * ItemModule
	 * Module which openes, creates, saves and deletes an item. It 
	 * extends the Module class.
	 */
	class ItemModule extends Module
	{
		/**
		 * Constructor
		 * @param int $id unique id.
		 * @param array $data list of all actions.
		 */
		function ItemModule($id, $data)
		{
			parent::Module($id, $data);
		}
		
		/**
		 * Executes all the actions in the $data variable.
		 * @return boolean true on success of false on fialure.
		 */
		function execute()
		{
			$result = false;
			
			foreach($this->data as $action)
			{
				if(isset($action["attributes"]) && isset($action["attributes"]["type"])) {
					$store = $this->getActionStore($action);

					$parententryid = $this->getActionParentEntryID($action);
					$entryid = $this->getActionEntryID($action);
				
					switch($action["attributes"]["type"])
					{
						case "open":
							$result = $this->open($store, $entryid, $action);
							break;
						case "save":
							$result = $this->save($store, $parententryid, $action);
							break;
						case "delete":
							$result = $this->delete($store, $parententryid, $entryid, $action);
							$GLOBALS["operations"]->publishFreeBusy($store, $parententryid);
							break;
						case "read_flag":
							$result = $this->setReadFlag($store, $parententryid, $entryid, $action);
							break;
						case "checknames":
							$result = $this->checkNames($store, $action);
							break;
						case "cancelMeetingRequest":
						case "declineMeetingRequest":
						case "acceptMeetingRequest":
							$message = $GLOBALS["operations"]->openMessage($store, $entryid);
							$basedate = (isset($action['basedate']) ? $action['basedate'] : false);
							$delete = false;
							/**
							 * Get message class from original message. This can be changed to 
							 * IPM.Appointment if the item is a Meeting Request in the maillist. 
							 * After Accepting/Declining the message is moved and changed.
							 */
							$originalMessageProps = mapi_getprops($message, array(PR_MESSAGE_CLASS));
							$req = new Meetingrequest($store, $message, $GLOBALS["mapisession"]->getSession());

							if($action["attributes"]["type"] == 'acceptMeetingRequest') {      				
								$tentative = isset($action["tentative"]) ? $action["tentative"] : false;
								$newProposedStartTime = isset($action["proposed_starttime"]) ? $action["proposed_starttime"] : false;
								$newProposedEndTime = isset($action["proposed_endtime"]) ? $action["proposed_endtime"] : false;
								$body = isset($action["body"]) ? $action["body"] : false;

								// sendResponse flag if it is set then send the mail response to the organzer.
								$sendResponse = true;
								if(isset($action["noResponse"]) && $action["noResponse"] == "true") {
									$sendResponse = false;
								}

								// When accepting occurrence by opening occurrence itself , we dont move occurrence.
								$move = (stristr($originalMessageProps[PR_MESSAGE_CLASS], 'IPM.Schedule.Meeting') !== false) ? true : false;
								
								// get the new entry id 
								$newEntryid = $req->doAccept($tentative, $sendResponse, $move, $newProposedStartTime, $newProposedEndTime, $body, true, $store, $basedate);
							} else {
								$delete = $req->doDecline(true, $store, $basedate);
							}


							// Publish updated free/busy information
							$GLOBALS["operations"]->publishFreeBusy($store);

							/**
							 * Now if the item is the Meeting Request that was sent to the attendee 
							 * it is removed when the user has clicked on Accept/Decline. If the 
							 * item is the appointment in the calendar it will not be moved. To only
							 * notify the bus when the item is a Meeting Request we are going to 
							 * check the PR_MESSAGE_CLASS and see if it is "IPM.Meeting*".
							 */
							$messageProps = mapi_getprops($message, array(PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_MESSAGE_CLASS));
							if(stristr($originalMessageProps[PR_MESSAGE_CLASS], 'IPM.Schedule.Meeting') !== false || $delete){
								$GLOBALS["bus"]->notify(bin2hex($messageProps[PR_PARENT_ENTRYID]), TABLE_DELETE, $messageProps); // send TABLE_DELETE event because the message has moved
							}

							if ($action["attributes"]["type"] == "declineMeetingRequest" && !$delete)
								$GLOBALS["bus"]->notify(bin2hex($messageProps[PR_PARENT_ENTRYID]), TABLE_SAVE, $messageProps); // send TABLE_SAVE event because an occurrence is deleted
							break;
						case "cancelInvitation":
							$GLOBALS["operations"]->cancelInvitation($store, $entryid, $action);
							
							break;
						case "removeFromCalendar":
							$GLOBALS["operations"]->removeFromCalendar($store, $entryid);
							break;

						case "resolveConflict":
							$result = $this->resolveConflict($store, $parententryid, $entryid, $action);
							break;
							
					}
				}
			}
			
			return $result;
		}
		
		/**
		 * Function which opens an item.
		 * @param object $store MAPI Message Store Object
		 * @param string $entryid entryid of the message
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function open($store, $entryid, $action)
		{
			$result = false;
			
			if($store && $entryid) {
				$data = array();
				$data["attributes"] = array("type" => "item");
				$message = $GLOBALS["operations"]->openMessage($store, $entryid);
				if (isset($this->plaintext) && $this->plaintext){
					$data["item"] = $GLOBALS["operations"]->getMessageProps($store, $message, $this->properties, true);
				}else{
					$data["item"] = $GLOBALS["operations"]->getMessageProps($store, $message, $this->properties, false);
				}
				
				if(isset($data["item"]["message_class"]) && $data["item"]["message_class"] == "IPM.DistList") {
					// remove non-client props
					unset($data["item"]["members"]);
					unset($data["item"]["oneoff_members"]);
					
					// get members
					$messageProps = mapi_getprops($message, array($this->properties["members"], $this->properties["oneoff_members"]));
					$members = isset($messageProps[$this->properties["members"]]) ? $messageProps[$this->properties["members"]] : array();
					$oneoff_members = isset($messageProps[$this->properties["oneoff_members"]]) ? $messageProps[$this->properties["oneoff_members"]] : array();

					// parse oneoff members
					foreach($oneoff_members as $key=>$item) {
						$oneoff_members[$key] = mapi_parseoneoff($item);
					}

					$data["item"]["members"]["member"] = array();
					$count = 0;

					foreach($members as $key=>$item) {
						$parts = unpack("Vnull/A16guid/Ctype/A*entryid", $item);

						if($parts["entryid"] != null) {
							if ($parts["guid"] == hex2bin("812b1fa4bea310199d6e00dd010f5402")) {
								$item = mapi_parseoneoff($item);
								$item["distlisttype"] = "ONEOFF";
								$item["entryid"] = "oneoff_" . (++$count). "_" . bin2hex($members[$key]);
								$item["icon_index"] = 512;
								$item["message_class"] = "IPM.DistListItem.OneOffContact";
							} else {
								$item = array();
								$item["name"] = $oneoff_members[$key]["name"];
								$item["type"] = $oneoff_members[$key]["type"];
								$item["address"] = $oneoff_members[$key]["address"];
								$item["entryid"] = array("attributes" => array("type" => "binary"), "_content" => bin2hex($parts["entryid"]));

								$updated_info = $this->updateItem($store, $oneoff_members[$key], $parts);
								if ($updated_info){
									$item["name"] = $updated_info["name"];
									$item["type"] = $updated_info["type"];
									$item["address"] = $updated_info["email"];
								}else{
									$item["missing"] = "1";
								}
								
								switch($parts["type"]) {
									case DL_USER:
										$item["distlisttype"] = "DL_USER";
										$item["icon_index"] = 512;
										$item["message_class"] = "IPM.Contact";
										break;
									case DL_USER2:
										$item["distlisttype"] = "DL_USER2";
										$item["icon_index"] = 512;
										$item["message_class"] = "IPM.Contact";
										break;
									case DL_USER3:
										$item["distlisttype"] = "DL_USER3";
										$item["icon_index"] = 512;
										$item["message_class"] = "IPM.Contact";
										break;
									case DL_USER_AB:
										$item["distlisttype"] = "DL_USER_AB";
										$item["icon_index"] = 512;
										$item["message_class"] = "IPM.DistListItem.AddressBookUser";
										break;
									case DL_DIST:
										$item["distlisttype"] = "DL_DIST";
										$item["icon_index"] = 514;
										$item["message_class"] = "IPM.DistList";
										break;
									case DL_DIST_AB:
										$item["distlisttype"] = "DL_DIST_AB";
										$item["icon_index"] = 514;
										$item["message_class"] = "IPM.DistListItem.AddressBookGroup";
										break;
								}
							}

							$item["name"] = w2u($item["name"]);
							$item["address"] = w2u($item["address"]);

							$item["message_flags"] = 1;
							array_push($data["item"]["members"]["member"], $item);
						}
					}
				}

				// Check for meeting request, do processing if necessary
				$req = new Meetingrequest($store, $message, $GLOBALS["mapisession"]->getSession());
				if($req->isMeetingRequestResponse()) {
					if($req->isLocalOrganiser()) {
						// We received a meeting request response, and we're the organiser
						$req->processMeetingRequestResponse();
						mapi_savechanges($message);
					}
				} else if($req->isMeetingRequest()) {
					if(!$req->isLocalOrganiser()) {
						if ($req->isMeetingOutOfDate()) {
							$data["item"]["out_of_date"] = true;
							$messageProps = mapi_getprops($message, array(PR_ENTRYID, PR_PARENT_ENTRYID, PR_STORE_ENTRYID));
							$GLOBALS["bus"]->notify(bin2hex($messageProps[PR_PARENT_ENTRYID]), TABLE_SAVE, $messageProps);
						} else {
							/**
							 * put item in calendar 'tentatively' if this is latest update,
							 * i.e if meeting request is not out of date.
							 */
							$req->doAccept(true, false, false);
							// Publish updated free/busy information
							$GLOBALS["operations"]->publishFreeBusy($store);
						}

						// We received a meeting request, show it to the user
						$data["item"]["ismeetingrequest"] = 1;
					}
				} else if($req->isMeetingCancellation()) {
					if(!$req->isLocalOrganiser()) {
						// Let's do some processing of this Meeting Cancellation Object we received
						$req->processMeetingCancellation();
						// We received a cancellation request, show it to the user
						$data["item"]["ismeetingcancel"] = 1;
					}
				}
				
				// Open embedded message in embedded message in ...
				if(isset($action["rootentryid"]) && isset($action["attachments"])) {
					if(isset($action["attachments"]["attach_num"]) && is_array($action["attachments"]["attach_num"])) {
						$data["item"] = $GLOBALS["operations"]->getEmbeddedMessage($store, $entryid, $this->properties, $action["attachments"]["attach_num"]);
					}
				}

				// check if this message is a NDR (mail)message, if so, generate a new body message
				if(isset($data["item"]["message_class"]) && $data["item"]["message_class"] == "REPORT.IPM.Note.NDR"){
					$data["item"]["body"] = $GLOBALS["operations"]->getNDRbody($GLOBALS["operations"]->openMessage($store, $entryid));
				}
								
				array_push($this->responseData["action"], $data);
				$GLOBALS["bus"]->addData($this->responseData);
				
				$result = true;
			}
			
			return $result;
		}
		
		/**
		 * Function which saves an item.
		 * @param object $store MAPI Message Store Object
		 * @param string $parententryid parent entryid of the message
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function save($store, $parententryid, $action)
		{
			$result = false;
			
			if(isset($action["props"])) {
				
				if(!$store && !$parententryid) {
					if(isset($action["props"]["message_class"])) {
						$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
						$parententryid = $this->getDefaultFolderEntryID($store, $action["props"]["message_class"]);
					}
				}else if(!$parententryid){
					if(isset($action["props"]["message_class"]))
						$parententryid = $this->getDefaultFolderEntryID($store, $action["props"]["message_class"]);
				}

				if($store && $parententryid) {
					$messageProps = array(); // returned props
					$result = $GLOBALS["operations"]->saveMessage($store, $parententryid, Conversion::mapXML2MAPI($this->properties, $action["props"]), false, (isset($action["dialog_attachments"])?$action["dialog_attachments"]:null), $messageProps);
					
					if($result) {
						$GLOBALS["bus"]->notify(bin2hex($parententryid), TABLE_SAVE, $messageProps);
					}
				}
			}
			
			return $result;
		}
		
		/**
		 * Function which deletes an item.
		 * @param object $store MAPI Message Store Object
		 * @param string $parententryid parent entryid of the message
		 * @param string $entryid entryid of the message		 
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function delete($store, $parententryid, $entryid, $action)
		{
			$result = false;
			
			if($store && $parententryid && $entryid) {
				$props = array();
				$props[PR_PARENT_ENTRYID] = $parententryid;
				$props[PR_ENTRYID] = $entryid;
	
				$storeprops = mapi_getprops($store, array(PR_ENTRYID));
				$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
				
				$result = $GLOBALS["operations"]->deleteMessages($store, $parententryid, $entryid);
				
				if($result) {
					$GLOBALS["bus"]->notify(bin2hex($parententryid), TABLE_DELETE, $props);
				}
			}
		
			return $result;
		}
		
		/**
		 * Function which sets the PR_MESSAGE_FLAGS property of an item.
		 * @param object $store MAPI Message Store Object
		 * @param string $parententryid parent entryid of the message
		 * @param string $entryid entryid of the message		 
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function setReadFlag($store, $parententryid, $entryid, $action)
		{
			$result = false;
			
			if($store && $parententryid && $entryid) {
				$flags = "read,noreceipt";
				if(isset($action["flag"])) {
					$flags = $action["flag"];
				}
				
				$props = array();
				$result = $GLOBALS["operations"]->setMessageFlag($store, $entryid, $flags, $props);
	
				if($result) {
					$GLOBALS["bus"]->notify(bin2hex($parententryid), TABLE_SAVE, $props);
				}
			}
			
			return $result;
		}
				
		/**
		 * Function which returns the entryid of a default folder.		 		 		 
		 * @param object $store MAPI Message Store Object
		 * @param string $messageClass the class of the folder
		 * @return string entryid of a default folder, false if not found		 		 
		 */
		function getDefaultFolderEntryID($store, $messageClass)
		{
			$entryid = false;
			
			if($store) {
				$rootcontainer = mapi_msgstore_openentry($store);
				$rootcontainerprops = mapi_getprops($rootcontainer, array(PR_IPM_DRAFTS_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID));

				switch($messageClass)
				{
					case "IPM.Appointment":
						if(isset($rootcontainerprops[PR_IPM_APPOINTMENT_ENTRYID])) {
							$entryid = $rootcontainerprops[PR_IPM_APPOINTMENT_ENTRYID];
						}
						break;
					case "IPM.Contact":
					case "IPM.DistList":
						if(isset($rootcontainerprops[PR_IPM_CONTACT_ENTRYID])) {
							$entryid = $rootcontainerprops[PR_IPM_CONTACT_ENTRYID];
						}
						break;
					case "IPM.StickyNote":
						if(isset($rootcontainerprops[PR_IPM_NOTE_ENTRYID])) {
							$entryid = $rootcontainerprops[PR_IPM_NOTE_ENTRYID];
						}
						break;
					case "IPM.Task":
						if(isset($rootcontainerprops[PR_IPM_TASK_ENTRYID])) {
							$entryid = $rootcontainerprops[PR_IPM_TASK_ENTRYID];
						}
						break;
					default:
						if(isset($rootcontainerprops[PR_IPM_DRAFTS_ENTRYID])) {
							$entryid = $rootcontainerprops[PR_IPM_DRAFTS_ENTRYID];
						}
						break;
				}
			}
			
			return $entryid;
		}
		
		function resolveConflict($store, $parententryid, $entryid, $action)
		{
			
			if(!is_array($entryid)) {
				$entryid = array($entryid);
			}
			$srcmessage = mapi_openentry($GLOBALS["mapisession"]->getSession(), $entryid[0], 0);
			if(!$srcmessage)
				return false;
			
			$dstmessage = mapi_openentry($GLOBALS["mapisession"]->getSession(), hex2bin($action["conflictentryid"]), MAPI_MODIFY);
			if(!$dstmessage)
				return false;
			
			$srcfolder = mapi_openentry($GLOBALS["mapisession"]->getSession(), $parententryid, MAPI_MODIFY);
			
			$result = mapi_copyto($srcmessage, array(), array(PR_CONFLICT_ITEMS, PR_SOURCE_KEY, PR_CHANGE_KEY, PR_PREDECESSOR_CHANGE_LIST), $dstmessage);
			if(!$result)
				return $result;
				
			//remove srcmessage entryid from PR_CONFLICT_ITEMS
			$props = mapi_getprops($dstmessage, array(PR_CONFLICT_ITEMS));
			if(isset($props[PR_CONFLICT_ITEMS])){
				$binentryid = hex2bin($entryid[0]);
				foreach($props[PR_CONFLICT_ITEMS] as $i => $conflict){
					if($conflict == $binentryid){
						array_splice($props[PR_CONFLICT_ITEMS],$i,1);
					}else{
						$tmp = mapi_openentry($GLOBALS["mapisession"]->getSession(), $conflict, 0);
						if(!$tmp){
							array_splice($props[PR_CONFLICT_ITEMS],$i,1);
						}
						unset($tmp);
					}
				}
				if(count($props[PR_CONFLICT_ITEMS]) == 0){
					mapi_setprops($dstmessage, $props);
				}else{
					mapi_deleteprops($dstmessage, array(PR_CONFLICT_ITEMS));
				}
			}
			
				
			mapi_savechanges($dstmessage);
			
			$result = mapi_folder_deletemessages($srcfolder, $entryid);
			
			$props = array();
			$props[PR_PARENT_ENTRYID] = $parententryid;
			$props[PR_ENTRYID] = $entryid[0];
			
			$storeprops = mapi_getprops($store, array(PR_ENTRYID));
			$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
			$GLOBALS["bus"]->notify(bin2hex($parententryid), TABLE_DELETE, $props);
			
			if(!$result)
				return $result;
		}

		function updateItem($store, $oneoff, $parts) {
			$result = false;
			$number = 1; // needed for contacts
			switch($parts["type"]) {
				case DL_USER3:
					$number++;
				case DL_USER2:
					$number++;
				case DL_USER:
					$item = mapi_msgstore_openentry($store, $parts["entryid"]);
					if(mapi_last_hresult() == NOERROR) {
						$properties = $GLOBALS["properties"]->getContactProperties();
						$props = mapi_getprops($item, $properties);
						if(is_int(array_search(($number - 1), $props[$properties["address_book_mv"]])) &&
							isset($props[$properties["email_address_" . $number]]) &&
							isset($props[$properties["email_address_display_name_" . $number]]) &&
							isset($props[$properties["email_address_type_" . $number]])){

							$result = array(
										"name" => $props[$properties["email_address_display_name_" . $number]],
										"email" => $props[$properties["email_address_" . $number]],
										"type" => $props[$properties["email_address_type_" . $number]]
									);
						}
					}
					break;
				case DL_DIST:
					$item = mapi_msgstore_openentry($store, $parts["entryid"]);
					if(mapi_last_hresult() == NOERROR) {
						$props = mapi_getprops($item, array(PR_DISPLAY_NAME));
						$result = array(
									"name" => $props[PR_DISPLAY_NAME],
									"email" => $props[PR_DISPLAY_NAME],
									"type" => "SMTP"
								);
					}
					break;
				case DL_USER_AB:
				case DL_DIST_AB:
					$ab = $GLOBALS["mapisession"]->getAddressBook();
					$item = mapi_ab_openentry($ab, $parts["entryid"]);
					if(mapi_last_hresult() == NOERROR) {
						$props = mapi_getprops($item, array(PR_DISPLAY_NAME, PR_SMTP_ADDRESS, PR_ADDRTYPE));
						$result = array(
									"name" => $props[PR_DISPLAY_NAME],
									"email" => $props[PR_SMTP_ADDRESS],
									"type" => $props[PR_ADDRTYPE]
								);
					}
					break;
			}
			return $result;
		}
	}
?>
