<?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
	/**
	 * Mail Module
	 */
	class MailListModule extends ListModule
	{
		/**
		 * @var array the special columns for a normal mail folder.
		 */
		var $mailcolumns;
		
		/**
		 * @var array the special columns for a sent mail folder.
		 */
		var $sentcolumns;

		// used by the search when using searchfolders
		var $searchActive;
		var $searchFolder;
		var $searchCriteriaCheck;
		var $searchResults;
		
		/**
		 * Constructor
		 * @param int $id unique id.
		 * @param array $data list of all actions.
		 */
		function MailListModule($id, $data)
		{
			$this->properties = $GLOBALS["properties"]->getMailProperties();
		
			// Default Columns
			$this->tablecolumns = array();
			$this->addColumn($this->tablecolumns, "importance", true, 0, _("Priority"), _("Sort On Priority"));
			$this->addColumn($this->tablecolumns, "icon_index", true, 1, _("Icon"), _("Sort On Icon"));
			$this->addColumn($this->tablecolumns, "hasattach", true, 2, _("Attachments"), _("Sort On Attachments"));
			$this->addColumn($this->tablecolumns, "subject", true, 4, _("Subject"), _("Sort On Subject"), PERCENTAGE);
			$this->addColumn($this->tablecolumns, "message_size", true, 6, _("Size"), _("Sort On Size"), 80);
			$this->addColumn($this->tablecolumns, "flag_icon", true, 7, _("Flag Status"), _("Sort On Flag Status"));
			$this->addColumn($this->tablecolumns, "categories", false, 8, _("Categories"), _("Sort On Categories"));

			// Columns & Sort: Mail Folders
			$this->addColumn($this->tablecolumns, "sent_representing_name", false, 3, _("From"), _("Sort On Sender"));
			$this->addColumn($this->tablecolumns, "message_delivery_time", false, 5, _("Received"), _("Sort On Received Date"));
			
			// Columns & Sort: Outbox & Sent Items
			$this->addColumn($this->tablecolumns, "display_to", false, 3, _("To"), _("Sort On Recipient"));
			$this->addColumn($this->tablecolumns, "client_submit_time", false, 5, _("Sent"), _("Sort On Sent Date"));
			$this->addColumn($this->tablecolumns, "last_modification_time", false, 5, _("Modification"), _("Sort On Modification Date"));

			// folder name column, needed for search folders
			$this->addColumn($this->tablecolumns, "parent_entryid", false, 6, _("In Folder"), _("Sort Folder"), 90, "folder_name");

			parent::ListModule($id, $data, array(OBJECT_SAVE, TABLE_SAVE, TABLE_DELETE));

			$this->sort = array();
			
			$this->searchActive = false;
			$this->searchFolder = false;
			$this->searchCriteriaCheck = false;
		}

		/**
		 * Function which returns an entryid, which is used to register this module. 
		 * The maillistmodule has a special feature that it supports an empty store 
		 * entryid and folder entryid, which means the default inbox of your default 
		 * store. This is used when opening the initial view when loggin on to the 
		 * webaccess.
		 * It first tries to use the normal Module::getEntryID method, but if that 
		 * returns false it will look for the default values.
		 * @return string an entryid if found, false if entryid not found.
		 */
		function getEntryID(){
			$entryid = parent::getEntryID();
			if(!$entryid) {
				$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
				$inbox = mapi_msgstore_getreceivefolder($store);
				$props = mapi_getprops($inbox, array(PR_ENTRYID));
				$entryid = bin2hex($props[PR_ENTRYID]);
			}
			return $entryid;
		}

		/**
		 * Executes all the actions in the $data variable.
		 * @return boolean true on success of false on fialure.
		 */
		function execute()
		{
			$result = false;
			$GLOBALS['PluginManager']->triggerHook("server.module.maillistmodule.execute.before", array('moduleObject' =>& $this));

			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 "list":
							if ($this->searchActive) {
								$result = $this->search($store, $entryid, $action);
							} else {
								/**
								 * The maillist module has a special feature that it supports an empty
								 * store entryid and folder entryid, which means the default inbox
								 * of your default store. This is used when opening the initial view
								 * when logging on to the webaccess.
								 * this feature is only needed when action type is list
								 */
								if(!$store) {
									$store = $GLOBALS["mapisession"]->getDefaultMessageStore();
								}

								if(!$entryid) {
									// The maillistmodule implements it's own getEntryID function, so 
									// $this->entryid property should contain the correct entryid.
									$entryid = hex2bin($this->entryid);
								}

								$result = $this->messageList($store, $entryid, $action);
							}
							break;
						case "search":
							$result = $this->search($store, $entryid, $action);
							break;
						case "updatesearch":
							$result = $this->updatesearch($store, $entryid, $action);
							break;
						case "stopsearch":
							$result = $this->stopSearch($store, $entryid, $action);
							break;
						case "save":
							$result = $this->save($store, $parententryid, $action);
							break;
						case "read_flag":
							$result = $this->setReadFlag($store, $entryid, $action);
							break;
						case "delete":
							$result = $this->delete($store, $parententryid, $entryid, $action);
							break;
						case "copy":
							$result = $this->copy($store, $parententryid, $entryid, $action);
							break;
						case "cancelInvitation":
							$GLOBALS["operations"]->cancelInvitation($store, $entryid);
							break;
					}
				}
			}
			$GLOBALS['PluginManager']->triggerHook("server.module.maillistmodule.execute.after", array('moduleObject' =>& $this));
			
			return $result;
		}
	
		function search($store, $entryid, $action)
		{
			$this->searchFolderList = true; // Set to indicate this is not the normal folder, but a search result
			$this->isSearching = true;
			$searchRestriction = false;
			if(isset($action["restriction"])) {
				if(isset($action["restriction"]["start"])) {
					// Set start variable
					$this->start = (int) $action["restriction"]["start"];
				}
				if(isset($action["restriction"]["search"])) {
					// if the restriction is a associative array, it means that only one property is requested
					// so we must build an non-associative array arround it
					if (is_assoc_array($action["restriction"]["search"])){
						$action["restriction"]["search"] = Array($action["restriction"]["search"]);
					}

					$res_or = Array();
					foreach($action["restriction"]["search"] as $i=>$search){
						$prop = false;
						// convert search property to MAPI property
						switch($search["property"]){
							case "subject":
								$prop = PR_SUBJECT;
								break;
							case "body":
								$prop = PR_BODY;
								break;
							case "to":
								$prop = PR_DISPLAY_TO;
								break;
							case "cc":
								$prop = PR_DISPLAY_CC;
								break;
							case "sender_name":
								$prop = PR_SENDER_NAME;
								break;
							case "sender_email":
								$prop = PR_SENDER_EMAIL_ADDRESS;
								break;
							case "sent_representing_name":
								$prop = PR_SENT_REPRESENTING_NAME;
								break;
							case "sent_representing_email":
								$prop = PR_SENT_REPRESENTING_EMAIL_ADDRESS;
								break;
						}
						// build restriction
						if ($prop !== false){
							array_push($res_or, Array(RES_AND,
													Array(
														Array(RES_EXIST, // check first if the property exists
															Array(ULPROPTAG=>$prop
															)
														),
														Array(RES_CONTENT,
															Array(FUZZYLEVEL => FL_SUBSTRING|FL_IGNORECASE,
																ULPROPTAG=>$prop,
																VALUE => utf8_to_windows1252($search["value"])
															)
														)
													)
												)
									); // array_push: $res_or
						}
					}
					if (count($res_or)>0){
						$searchRestriction = Array(RES_OR,$res_or);
					}else{
						$searchRestriction = false;
					}
				}else{
					$searchRestriction = false;
				}
			}
				
			if ($searchRestriction == false){
				return $this->messageList($store, $entryid, $action);
			}

			$createFolder = true;
			if ($this->searchActive){
				$folder = mapi_msgstore_openentry($store, hex2bin($this->searchFolder));
				if (mapi_last_hresult()==NOERROR){
					$createFolder = false;
				}
			}
			
			if ($createFolder){
				$searchFolders = $this->getSearchFoldersRoot($store);
				if (!$searchFolders){
					return $this->messageList($store, $entryid, $action);
				}

				$folderName = $GLOBALS["operations"]->checkFolderNameConflict($store, $searchFolders, "WebAccess Search Folder");
				$folder = mapi_folder_createfolder($searchFolders, $folderName, null, 0, FOLDER_SEARCH);
			}
			
			// check if searchcriteria has changed
			$restrictionCheck = md5(serialize($searchRestriction));

			$flags = 0;
			if (isset($action["subfolders"]) && $action["subfolders"]=="true"){
				$flags = RECURSIVE_SEARCH;
			}
			$result = mapi_folder_getsearchcriteria($folder, 0);
			$old_flags = ($result["searchstate"] & RECURSIVE_SEARCH);

			if ($restrictionCheck != $this->searchCriteriaCheck || $old_flags != $flags){
				mapi_folder_setsearchcriteria($folder, $searchRestriction, array($entryid), $flags);
				$this->searchCriteriaCheck = $restrictionCheck;
			}
			
			$props = mapi_getprops($folder, array(PR_ENTRYID));
			$entryid = $props[PR_ENTRYID];
			
			unset($action["restriction"]);

			$this->searchActive = true;
			$this->searchFolder = bin2hex($entryid);

			// Sort
			$this->parseSortOrder($action);
				
			// List columns visible
			$this->parseVisibleColumns($action);
				
			// Create the data array, which will be send back to the client
			$data = array();
			$data["attributes"] = array("type" => "list");
			$data["column"] = $this->tablecolumns;

			$firstSortColumn = reset(array_keys($this->sort)); // get first key of the sort array
			$data["sort"] = array();
			$data["sort"]["attributes"] = array();
			$data["sort"]["attributes"]["direction"] = (isset($this->sort[$firstSortColumn]) && $this->sort[$firstSortColumn] == TABLE_SORT_ASCEND) ? "asc":"desc";
			$data["sort"]["_content"] = array_search($firstSortColumn, $this->properties);
				
			// Get the table and merge the arrays
			$table = $GLOBALS["operations"]->getTable($store, $entryid, $this->properties, $this->sort, $this->start, false, $this->searchRestriction);
			$data = array_merge($data, $table);

			$rowcount = $GLOBALS["settings"]->get("global/rowcount", 50);

			// remember which entryid's are send to the client
			$this->searchResults = array();
			foreach($table["item"] as $item){
				$result_entryid = $item["entryid"]["_content"];
				array_push($this->searchResults, $result_entryid);
			}
			array_push($this->responseData["action"], $data);

			$result = mapi_folder_getsearchcriteria($folder, 0);
			$data = array();
			$data["attributes"] = array("type" => "search");
			$data["searchfolderentryid"] = array("attributes"=>array("type"=>"binary"),"_content"=>$this->searchFolder);
			$data["searchstate"] = $result["searchstate"];
			$data["results"] = count($this->searchResults);

			array_push($this->responseData["action"], $data);

			$GLOBALS["bus"]->addData($this->responseData);
			return true;
		}

		function updatesearch($store, $entryid, $action)
		{
			// Only allow to return anything if the search is still going on
			if(!$this->isSearching){
				return true;
			}

			if (bin2hex($entryid) != $this->searchFolder){
				$data = array();
				$data["attributes"] = array("type" => "search");
				$data["searcherror"] = "Wrong Entryid";
				array_push($this->responseData["action"], $data);
				$GLOBALS["bus"]->addData($this->responseData);
				return true;
			}

			$folder = mapi_msgstore_openentry($store, hex2bin($this->searchFolder));
			$searchresult = mapi_folder_getsearchcriteria($folder, 0);
			$searchstate = $searchresult["searchstate"];
			$table = mapi_folder_getcontentstable($folder);

			$rowcount = $GLOBALS["settings"]->get("global/rowcount", 50);
			$numberOfResults = count($this->searchResults);

			$data = false;

			if ($numberOfResults<$rowcount){
				$items = mapi_table_queryallrows($table, array(PR_ENTRYID));
				foreach($items as $props){
					if (!in_array(bin2hex($props[PR_ENTRYID]),$this->searchResults)){
						$data = array();
						$data["attributes"] = array("type" => "item", "searchfolder" => $this->searchFolder);
						$data["item"] = $GLOBALS["operations"]->getMessageProps($store, $GLOBALS["operations"]->openMessage($store, $props[PR_ENTRYID]), $this->properties);
						array_push($this->responseData["action"], $data);
						array_push($this->searchResults, bin2hex($props[PR_ENTRYID]));
					}
					
					// when we have more results then fit in the client, we break here, we only need to update the counters from this point
					$numberOfResults = count($this->searchResults);
					if ($numberOfResults>=$rowcount){
						break; 
					}
				}
			}
			$numberOfResults = mapi_table_getrowcount($table);

			if($data)
    			array_push($this->responseData["action"], $data);

			$data = array();
			$data["attributes"] = array("type" => "search");
			$data["searchfolderentryid"] = array("attributes"=>array("type"=>"binary"),"_content"=>$this->searchFolder);
			$data["searchstate"] = $searchstate;
			$data["results"] = $numberOfResults;

			$data["page"] = array();
			$data["page"]["start"] = 0;
			$data["page"]["rowcount"] = $rowcount;
			$data["page"]["totalrowcount"] = $numberOfResults;

			array_push($this->responseData["action"], $data);
			$GLOBALS["bus"]->addData($this->responseData);

			return true;
		}


		function stopSearch($store, $entryid, $action)
		{
			$this->isSearching = false;

			$folder = mapi_msgstore_openentry($store, hex2bin($this->searchFolder));
			$searchresult = mapi_folder_getsearchcriteria($folder, 0);
			// This line stops the search
			mapi_folder_setsearchcriteria($folder, $searchresult['restriction'], $searchresult['folderlist'], STOP_SEARCH);
		}

		function getSearchFoldersRoot($store)
		{
			$folder = true;

			// check if we can create search folders
			$storeProps = mapi_getprops($store, array(PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID));
			if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK)!=STORE_SEARCH_OK) {
				$folder = false;
			}

			if ($folder){
				// open search folders root
				$folder = mapi_msgstore_openentry($store, $storeProps[PR_FINDER_ENTRYID]);
				if (mapi_last_hresult()!=0){
					$folder = false;
				}
			}

			return $folder;
		}


		/**
		 * Function which retrieves a list of messages in a folder. It verifies if
		 * the given entryid is a default send folder, like outbox, sentmail of draft 
		 * folder. If so, different columns should be visible (to and submit time).		 
		 * @param object $store MAPI Message Store Object
		 * @param string $entryid entryid of the folder
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function messageList($store, $entryid, $action)
		{
			$this->searchFolderList = false; // Set to indicate this is not the search result, but a normal folder content
			// begin column changes

			// When it is a searchfolder we want the orignal folder column
			if ($this->searchActive){
				$this->tablecolumns[$this->getColumn($this->tablecolumns, "parent_entryid")]["visible"] = true;
			}else{
				$this->tablecolumns[$this->getColumn($this->tablecolumns, "parent_entryid")]["visible"] = false;
			}

			// When this folder is the Outbox, Sent Mail or Drafts folder we want different default columns
			$props = mapi_getprops($store, array(PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID));
			$rootcontainer = mapi_msgstore_openentry($store);
			$props = array_merge($props, mapi_getprops($rootcontainer, array(PR_IPM_DRAFTS_ENTRYID)));
			
			$inbox = mapi_msgstore_getreceivefolder($store);
			$inboxprops = mapi_getprops($inbox, array(PR_ENTRYID));
			$inboxentryid = $inboxprops[PR_ENTRYID];
			
			if(array_search($entryid, $props) !== false) {
				if(($key = $this->getColumn($this->tablecolumns, "sent_representing_name")) !== false) {
					$this->tablecolumns[$key]["visible"] = false;
				}
				
				if(($key = $this->getColumn($this->tablecolumns, "message_delivery_time")) !== false) {
					$this->tablecolumns[$key]["visible"] = false;
				}
				
				$this->tablecolumns[$this->getColumn($this->tablecolumns, "display_to")]["visible"] = true;
				$this->tablecolumns[$this->getColumn($this->tablecolumns, "client_submit_time")]["visible"] = true;
				$this->sort[$this->properties["client_submit_time"]] = TABLE_SORT_DESCEND;
			} else {
				if(($key = $this->getColumn($this->tablecolumns, "display_to")) !== false) {
					$this->tablecolumns[$key]["visible"] = false;
				}
				
				if(($key = $this->getColumn($this->tablecolumns, "client_submit_time")) !== false) {
					$this->tablecolumns[$key]["visible"] = false;
				}
				
				$this->tablecolumns[$this->getColumn($this->tablecolumns, "sent_representing_name")]["visible"] = true;
				$this->tablecolumns[$this->getColumn($this->tablecolumns, "message_delivery_time")]["visible"] = true;
				$this->sort[$this->properties["message_delivery_time"]] = TABLE_SORT_DESCEND;
			}
			
			// end column changes

			// Restriction
			$this->parseSearchRestriction($action);

			// Sort
			$this->parseSortOrder($action);
				
			// List columns visible
			$this->parseVisibleColumns($action);
				
			// Create the data array, which will be send back to the client
			$data = array();
			$data["attributes"] = array("type" => "list");
			$data["column"] = $this->tablecolumns;

			$firstSortColumn = reset(array_keys($this->sort)); // get first key of the sort array
			$data["sort"] = array();
			$data["sort"]["attributes"] = array();
			$data["sort"]["attributes"]["direction"] = (isset($this->sort[$firstSortColumn]) && $this->sort[$firstSortColumn] == TABLE_SORT_ASCEND) ? "asc":"desc";
			$data["sort"]["_content"] = array_search($firstSortColumn, $this->properties);
				
            // Return some information about the folder. The entryid and storeid may be unknown to the client if it passed
            // an empty storeid and folder entryid.
			$folder = mapi_msgstore_openentry($store, $entryid);
			$folderprops = mapi_getprops($folder, array(PR_DISPLAY_NAME, PR_ENTRYID, PR_STORE_ENTRYID, PR_PARENT_ENTRYID));
			
			//check if the folder is not the root folder.
			if($entryid != $folderprops[PR_ENTRYID]){
				$data["folder_title"] = array();
				$data["folder_title"]["_content"] = w2u($folderprops[PR_DISPLAY_NAME]);
			}
			$data["entryid"] = array();
			$data["entryid"]["_content"] = bin2hex($folderprops[PR_ENTRYID]);
			$data["storeid"] = array();
			$data["storeid"]["_content"] = bin2hex($folderprops[PR_STORE_ENTRYID]);
			$data["isinbox"] = array();
			$data["isinbox"]["_content"] = $folderprops[PR_ENTRYID] == $inboxentryid;
				
			// Get the table and merge the arrays
			$data = array_merge($data, $GLOBALS["operations"]->getTable($store, $entryid, $this->properties, $this->sort, $this->start, false, $this->searchRestriction));
				
			array_push($this->responseData["action"], $data);
			$GLOBALS["bus"]->addData($this->responseData);
		}
	}
?>
