<?php 
/*
* Copyright (c) 2009-2010 ATMAIL. All rights reserved
* See http://atmail.com/license.php for license agreement
*/

class Atmail_Mail_Storage_Pop3 extends Zend_Mail_Storage_Pop3
{	
	public $mailStorageType = 'POP3';
	public $dataSource = 'unknown';
	public $updateCacheStamp = 0;
	protected $_messageClass = 'Atmail_Mail_Message';
	private $_defaultNamespaceName = 'Atmail_Mail_Storage_Pop3';
	private $_namespaceName = null;
	private $_config;
	private $_cache = null;
	private $_connected = false;
	private $_messagesFetched = false;
	private $_messagesHeadersFetched = false;
	private $_messages = null; //list of POP3 (INBOX) messages keyed by array position, contains array(ordinalid, uniqueid, _headers, ....)
	private $_sort = 'unknown';
 	private $_order = 'unknown';
	private $_checkLocalFoldersExistDone = false;
	
	public function __construct(&$config)
	{
		$this->_config = $config;
		if(isset($this->_config['namespaceName']))
			$this->_namespaceName = $this->_config['namespaceName'];
		else
			$this->_namespaceName = $_defaultNamespace;
	}
	
	public function connect()
	{
		if($this->_connected)
			return;
		$this->_connected = true;
		parent::__construct($this->_config);
		$this->_messagesHeadersFetched = false;
		$this->_messagesFetched = false; 
 	}

	public function close()
	{
		if($this->_connected)
			parent::close();
		$this->_connected = false;
	}
	
	// MAGIC FUNCTIONS
	////////////////////////////////////////////////////////////////////////////////
    
	public function current() //overloaded the class iterator to handle overloading getMessage to use uniqueId
	{
		return $this->getMessage( $this->getUniqueId($this->_iterationPos) );
	}
	
	public function getMessage($uniqueId)
	{
		$this->connect();
		
		$ordinalId = $this->getOrdinalIdByUniqueId($uniqueId);
		if(!$ordinalId)
			throw new Atmail_Mail_Exception('Error retrieving message. Message ( ' . $uniqueId . ':' . $ordinalId . ') not found.');
		return parent::getMessage($ordinalId);
	}
	
	public function removeMessage($uniqueId)
	{
		$this->connect();
		$ordinalId = $this->getOrdinalIdByUniqueId($uniqueId);
		if(!$ordinalId)
			throw new Atmail_Mail_Exception('Message ( ' . $uniqueId . ':' . $ordinalId . ') not found.');
	   	parent::removeMessage($ordinalId);
		$_messagesC = $this->countMessages();
		$foundAt = $_messagesC;
		for( $a=0 ; $a<$_messagesC ; $a++) 
		{
			if($this->_messages[$a]['uniqueid'] == $uniqueId) {
				$foundAt = $a;
				$temp = $this->_messages[$a];			
			}
			if( $a > $foundAt)
				$this->_messages[$a-1] = $this->_messages[$a];
			if( $a >= $foundAt && $a == ($_messagesC-1) ) //if found and at last message
				array_pop($this->_messages);
		}
	}
		
	public function getFreshUniqueIdByOrdinalId($ordinalId = null)
	{
		$this->connect();
		if( isset($ordinalId) )
			return parent::getUniqueId($ordinalId);		
		$this->_messages = array();
		foreach(parent::getUniqueId() as $ordinalid => $uid)
	 		$this->_messages[] = array('ordinalid' => $ordinalid, 'uniqueid' => $uid);
    	$this->_sort = 'unknown';
		$this->_order = 'unknown';
		$this->dataSource = 'fresh';
		$this->_messagesFetched = true;
		$this->_updateCacheStamp = time();
		return $this->_messages;
	}
	
	public function getUniqueIdByOrdinalId($ordinalid = null)
	{
		if(	!$this->_messagesFetched )
			return $this->getFreshUniqueIdByOrdinalId($ordinalid);
		if( isset($ordinalid) )
			foreach( $this->_messages as $pos => $message)
				if( $message['ordinalid'] == $ordinalid )
					return $message['uniqueid'];
	   	else
	   		return $this->_messages; // a bit lazy but returns entire array including required uniqueids
	}
	
	public function getOrdinalIdByUniqueId($uniqueId)
	{
		if( $this->_messagesFetched ) {
			$_messagesC = $this->countMessages();
			for( $a=0 ; $a<$_messagesC ; $a++) {
				if($this->_messages[$a]['uniqueid'] == $uniqueId)
					return $this->_messages[$a]['ordinalid'];
			}  
		} else {
			$this->connect();
			return parent::getNumberByUniqueId($uniqueId);
		}
		throw new Atmail_Mail_Exception('No ordinalId found for uniqueId:' . $uniqueId);
	}
	
	public function getFreshMessageList()
	{
		return $this->getFreshUniqueIdByOrdinalId();
	}
	
	public function getMessageList()
	{
		if(	!$this->_messagesFetched )
			return $this->getFreshMessageList();
	    else
			return $this->_messages;
	}
	
	public function countMessages()
	{
		$this->getUniqueIdByOrdinalId();
		return count($this->_messages); 
	}
	
	
	public function getFreshMessagesWithHeaders()
	{
		$this->getMessageList();
		$_messagesC = $this->countMessages();
		for( $a=0 ; $a<$_messagesC ; $a++ )
		{
			//echo 'AA:' . $a . ':' . $this->_messages[$a]['ordinalid'] . ':' . $this->_messages[$a]['uniqueid'] . "<br />\n";
			$messageClassInstance = $this->getMessage($this->_messages[$a]['uniqueid']);
			$this->_messages[$a]['_headers'] = $messageClassInstance->processHeaders( array('ordinalid' => $this->_messages[$a]['ordinalid'], 
																							'uniqueid' => $this->_messages[$a]['uniqueid']) );
			//echo 'BB:' . $a . ':' . $this->_messages[$a]['ordinalid'] . ':' . $this->_messages[$a]['uniqueid'] . "<br />\n";
			
			/* 	IMPORTANT: headers are stored in private _messages so getHeadersReference will return common access interface, 
			    even although internals are different for the various stores
				NB $a here is array position, not ordinal position or UniqueID
			*/
		}
		//ddie($this->_messages, __METHOD__ . '#' . __LINE__ . ': $this->_messages');
		
        $this->_messagesHeadersFetched = true;
		return $this->_messages;
	}
	
	public function getMessagesHeaders()
	{
		if(!$this->_messagesHeadersFetched)
			$this->getFreshMessagesWithHeaders();
		return $this->_messages;
	}
	
	public function getMessageHeaders($uniqueId)
	{
		//TODO: toplines
		if($this->_messagesHeadersFetched) {
			$_messagesC = count($this->_messages);
			for( $a=0 ; $a<$_messagesC ; $a++ )
			{
				if( $this->_messages[$a]['uniqueid'] == $uniqueId)
					return $this->_messages[$a]['_headers'];
			}
		} else {
			return $this->getMessage($uniqueId)->processHeaders( array('uniqueid' => $uniqueId, 
																	   'ordinalid' => 'notimplimented cos needs to build entire message list to get ordinalid') );
		}
	}

	public function getRawHeader($uniqueId, $part = null, $toplines = null)
	{
		return parent::getRawHeader( $this->getOrdinalIdByUniqueId($uniqueId), $part, $toplines );
	}   
	
	public function getRawContent($uniqueId, $part = null)
	{
		return parent::getRawContent( $this->getOrdinalIdByUniqueId($uniqueId), $part );
	}   
	
	// CACHE FUNCTIONS
	////////////////////////////////////////////////////////////////////////////////
	
	public function updateCache()
	{
		//This will also update new messages with get and process headers if _messagesHeadersFetched in old cache cos 90% usercase is list, 
		// else would loose all cache data and therefore value, but should make more generic if usage expands. (e.g. fetched flag for each message)
		//make copy of old then reconnect to storage and compare
		//only update what was cached, i.e., connect, then if !_messagesFetched then dont update list , if !_messagesHeadersFetched then dont update headers
		//consider preserving cached sort and order for performance (minor for short lists)
		if( $this->_messagesFetched ) {
			$oldCache_messages = $this->_messages; //in case needed for _messagesHeadersFetched 
			$oldCache_messagesFetched = $this->_messagesFetched;
			$oldCache_messagesHeadersFetched = $this->_messagesHeadersFetched;
			$this->close();
			$this->connect();
			$this->getFreshMessageList(); //fetch all from server (argument not specified)
			if($oldCache_messagesHeadersFetched) {
				$fresh_messagesC = count($this->_messages);
		        $oldCache_messagesC = count($oldCache_messages);
				for( $a=0 ; $a<$fresh_messagesC ; $a++ ) {
					//go through each new message trying to copy cached data across
					$foundInCache = false;
					for( $b=0 ; $b<$oldCache_messagesC ; $b++ ) {
						 if( $this->_messages[$a]['uniqueid'] == $oldCache_messages[$b]['uniqueid']) {
							$this->_messages[$a]['_headers'] = $oldCache_messages[$b]['_headers'];//NB only copy headers across cos ordinal may have changed
							$foundInCache = true;
							break; //break out of searching old cache once found
						 }
					}
					if( !$foundInCache )
						$this->_messages[$a]['_headers'] = $this->getMessage($a+1)->processHeaders( array('ordinalid' => $this->_messages[$a]['ordinalid'], 
																									  	  'uniqueid' => $this->_messages[$a]['uniqueid']) );
				}
			}
			$this->_messagesHeadersFetched = true;
		}
		$this->updateCacheStamp = time();
	}   

	// SORT FUNCTIONS
	////////////////////////////////////////////////////////////////////////////////

	public function sort($sort, $order)
	{
		$this->getMessagesHeaders();
		//handle folders with no messages
		if($this->countMessages() < 2)
		{
			$this->_sort = $sort;
			$this->_order = $order;
			return;
		}	
		if($this->_sort != $sort)
		{
			switch($sort)
			{
				case 'date':
					$sortFunction = 'sortByDate';
				    break;
				case 'subject':
					$sortFunction = 'sortBySubject'; 
					break;
				case 'to':
					$sortFunction = 'sortByTo';
				    break;
				case 'from':
					$sortFunction = 'sortByFrom'; 
					break;
				case 'size':
					$sortFunction = 'sortBySize'; 
					break;  
				case 'hasattachments':
					$sortFunction = 'sortByHasAttachments'; 
					break;  
				default:
					$sortFunction = 'sortByDate';
			}
			//$sortFunction = 'sortBySize'; //overide
			usort($this->_messages, array($this, $sortFunction));
			$this->_sort = $sort;
			//N.B. once usort is complete, array will be an asc order
			$this->_order = 'asc';
		}
		if($this->_order != $order)
		{
			$this->_order = $order;
			$this->_messages = array_reverse($this->_messages);
		}
	}
	
	private function sortBySubject($a,$b)
	{
		$sortable = array( strtolower($a['_headers']['subject']),strtolower($b['_headers']['subject']) );
        $sorted = $sortable;
        sort($sorted);   
        return ($sorted[0] == $sortable[0]) ? -1 : 1;
	}
	
	private function sortByDate($a,$b)
	{
		$sortable = array($a['_headers']['epoch'],$b['_headers']['epoch']);
        $sorted = $sortable;
        sort($sorted);   
        return ($sorted[0] == $sortable[0]) ? -1 : 1;
	}
	
	private function sortByTo($a,$b)
	{
		$sortable = array( strtolower($a['_headers']['to']),strtolower($b['_headers']['to']) );
		$sorted = $sortable;
        sort($sorted);   
        return ($sorted[0] == $sortable[0]) ? -1 : 1;
	}
	
	private function sortByFrom($a,$b)
	{
		$sortable = array( strtolower($a['_headers']['from']),strtolower($b['_headers']['from']) );
		$sorted = $sortable;
        sort($sorted);   
        return ($sorted[0] == $sortable[0]) ? -1 : 1;
	}
	
	private function sortBySize($a,$b)
	{
		$sortable = array($a['_headers']['sizeraw'],$b['_headers']['sizeraw']);
        $sorted = $sortable;
        sort($sorted);   
        return ($sorted[0] == $sortable[0]) ? -1 : 1;
	}
	
	private function sortByHasAttachments($a,$b)
	{
		$sortable = array($a['_headers']['hasattachments'],$b['_headers']['hasattachments']);
        $sorted = $sortable;
        sort($sorted);   
        return ($sorted[0] == $sortable[0]) ? -1 : 1;
	} 
	
	
	// SEARCH FUNCTIONS
	////////////////////////////////////////////////////////////////////////////////
	
	/**
	* Search an IMAP folder and returns an array of IMAP ordinal ids
	* A string, delimited by spaces, in which the following keywords are allowed. Any multi-word arguments (e.g. FROM "joey smith") must be quoted.
    * ALL - return all messages matching the rest of the criteria
    * ANSWERED - match messages with the \\ANSWERED flag set
    * BCC "string" - match messages with "string" in the Bcc: field
    * BEFORE "date" - match messages with Date: before "date"
    * BODY "string" - match messages with "string" in the body of the message
    * CC "string" - match messages with "string" in the Cc: field
    * DELETED - match deleted messages
    * FLAGGED - match messages with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set
    * FROM "string" - match messages with "string" in the From: field
    * KEYWORD "string" - match messages with "string" as a keyword
    * NEW - match new messages
    * OLD - match old messages
    * ON "date" - match messages with Date: matching "date"
    * RECENT - match messages with the \\RECENT flag set
    * SEEN - match messages that have been read (the \\SEEN flag is set)
    * SINCE "date" - match messages with Date: after "date"
    * SUBJECT "string" - match messages with "string" in the Subject:
    * TEXT "string" - match messages with text "string"
    * TO "string" - match messages with "string" in the To:
    * UNANSWERED - match messages that have not been answered
    * UNDELETED - match messages that are not deleted
    * UNFLAGGED - match messages that are not flagged
    * UNKEYWORD "string" - match messages that do not have the keyword "string"
    * UNSEEN - match messages which have not been read yet
	* LARGER - Messages with an [RFC-2822] size larger than the specified number of octets
	* SMALLER - Messages with an [RFC-2822] size larger than the specified number of octets
	*
	* Expects search args to be in the general search criteria array format
	* $args = array(); // array of search terms
	* $args[0] = array('fieldname', 'begins', 'searchtext', 'and'); //search term containing fieldname, match position, 
	* searchtext, boolean term to join with previous (first ignored)
	* NB IMAP search looks for messages matching ALL criteria, not ANY criteria. For example the search
	* TODO: upgrade query parser to enable ANY functionality as well
	* @return array of match ids by uid
	*/
	
	public function search($args)
	{
		return 'pop3 results, no inbox search'; //currently disabled
		// search capabilities to impliment: FROM TO CC BCC SUBJECT LARGER SMALLER SINCE BEFORE TEXT NOT
		// OR will return union of search result sets
		// AND will return intersection of
		// Folder search will search each folder
		// e.g. a UID SEARCH SUBJECT i TEXT i SINCE 1-Jan-2000 FROM com NOT TO za LARGER 1 SMALLER 1000000   
		if( !$this->_connected )
			$this->connect();
		/*
		  SUDO:
			for each $args as search term
			Create IMAP search string
			requestAndRepsonce search term
			Take all results and join as per and/or
			fetch message headers
			store search result and args in _searchResults
			//CONSIDER: _searchId, _searchArgs,  can be needed for saving session searches or for stateless ui implimentation	
		*/
		//TODO: impliment further culling with begins with ends with contains
		//TODO: impliment parsing search criteria to compose IMAP safe search string
		$ImapField = array('from' => 'FROM', 'to' => 'TO', 'subject' => 'SUBJECT', 'size' => 'LARGER', 'size' => 'SMALLER', 'date' => 'SINCE', 'date' => 'BEFORE', 'value' => 'TEXT');         
		$searchString = 'UID SEARCH';
		foreach($args['argLines'] as $term)
		{
			$valueString = $term['value'];
			//check for range terms and handle seperately
			if( $term['field'] == 'size' )
				if( $term['modifier'] == 'lessthan' )
					$field = 'SMALLER';
				else
					$field = 'LARGER';
			elseif( $term['field'] == 'date' )
				if( $term['modifier'] == 'lessthan' )
					$field = 'BEFORE';
				else
					$field = 'SINCE';
		    else {
				$field = $ImapField[$term['field']];
				$valueString = '"' . $valueString . '"';
			}			
			$searchString .= ' ' . $field . ' ' . $valueString;
		}
		$searchResults = $this->_protocol->requestAndResponse($searchString);
		//$searchResults = $this->_protocol->search( array('SUBJECT i') );
		if($searchResults == false)
			Zend_Registry::get('log')->info('Search failed: ' . $searchString);
		elseif(is_array($searchResults[0]) && count($searchResults[0]) == 0)
			return $searchResults[0];
		else
		{
			$searchResults = $searchResults[0];
			if ($searchResults[0] == 'SEARCH')
	        	array_shift($searchResults);
			$searchResultsC = count($searchResults);
			for( $a=0 ; $a<$searchResultsC ; $a++ )
			{
				$searchResults[$a] = array('uniqueid' => $searchResults[$a]);
				$searchResults[$a]['_headers'] = $this->getMessageHeaders($searchResults[$a]['uniqueid']);
			}
			return $searchResults;   
		}
	}
}