<?php 
/*
* Copyright (c) 2009-2010 ATMAIL. All rights reserved
* See http://atmail.com/license.php for license agreement
*/
//TODO: gracefully handle folder changes to invalid folder.
require_once 'Atmail/Mail/Protocol/Imap.php';
require_once('Mail/RFC822.php');
require_once 'library/Stopwatch.php';

class Atmail_Mail_Storage_Imap extends Zend_Mail_Storage_Imap {	
	
	public $mailStorageType = 'IMAP';
	public $dataSource = 'unknown';
	public $updateCacheStamp = 0;
	public $_folderStat; // Used for storage on folder select (new,unread,total)
	
	protected $_messageClass = 'Atmail_Mail_Message';
	protected $_protocol; 
	
	private $_sort = 'DATE';
	private $_order = 'REVERSE';
	private $_namespaceName = 'Atmail_Mail_Storage_Imap';
	private $_session;
	private $_config;
	private $_cache = null;
	private $_connected = false;
	private $_folders = false;
	private $_messages = null; //list of messages keyed by array position, contains array(ordinalid, uniqueid, _headers, ....) for messages in current folder
	private $_messagesBasicHeadersFetched = false;
	private $_messagesAllHeadersFetched = false;
	private $_threads = null; //multidimentional array of threads
	private $_foldersInfo = null;
	private $_sortMaxLimit = 30000;
	private $_threadingMaxLimit = 30000;
	private $_threadsLimitMonths = 0;
	private $_threadsEnabled = true; //N.B. multiple factors affect this, so only allow changing to false. (server capability, current folder size, user setting - none can force true, but any can force false)
	private $_folderNamespace;
	private $_store; //central store where all data is kept 
	private $_enableMessagePreview = true;
	
	/* about _store
	   //being implimented in stages to coexist with current, and old can be stripped out once done, tested, and trustworthy
	   Through storing all data in a central store it can be saved to cache and loaded again, and metadata can also be kept (age, sort, order, etc.)
		all large volumes or computationally expensive data (IMAP server or Apache Server) should be put in _store for management/caching
		
	   	general format is heirarchial based around the data, and also contains meta data
		if a _store node is missing metaData it is assumed that parent age is relevant
	
		dont timestamp dtat that wont/cant change. e.g different IMAP query string data results linked to a message/messages.
		where key contaims ids (possibly only partial set) then compose a cananonical unique unambigious name e.g. messagesBasicHeaders, or messagesBODYSTRUCTURE
		all sttored data should not be the raw responce but rather the processed results.
		
		any data that can change (local or 3rd party) should have a ageTimestamp ocnnected to it (e.g. message moved to another folder by a 3rd party client)
							 ** Folder **
		_store -> folders -> ageTimestamp
						  -> name		   
											** Messages **
						  -> messageList -> ageTimestamp
														** Message **
										 -> messages -> UID
									  				 -> BODYSTRUCTURE
													 -> fullMessageRaw - this will probably be stored on disk with unique id and folder - must be maintained by higher level functions
									  				 -> fullHeaders
													 -> basicHeaders
													 -> various cached IMAP command strings ir goot keyName as key e.g. UID FETCH AAA BBB CCC
													 -> CONSIDER: pushing sort fields here for convenient sorting/searching
						  -> sort
						  -> order
						  -> startUID
						  -> endUID
						  -> threadsStructure
						  -> EXAMINE
						  -> ...
						 
	   N.B. continue to use _session for small data items, like current folder etc, so can continue to fucntion if caching is disabled for any reason 
	   
	   when fetching data out of the store collect available data, then compose query only for missing data and fetch that from the servre and add to store before serving it.
	   donsider serving in 2 stages - cached data and then fetched data so UI feels more responsive, especially for comonly accessed data.
	*/								 
	
	
	//private $_threadsLinear = null; //linear array of UIDs in _threads
	
	/**
	* override the parent constructor to rather instantiate Atmail_Mail_Protocol_Imap
	* Supported paramters are
	*   - user username
	*   - host hostname or ip address of IMAP server [optional, default = 'localhost']
	*   - password password for user 'username' [optional, default = '']
	*   - port port for IMAP server [optional, default = 110]
	*   - ssl 'SSL' or 'TLS' for secure sockets
	*   - folder select this folder [optional, default = 'INBOX']
	*
	* @param  array $params mail reader specific parameters
	* @throws Zend_Mail_Storage_Exception
	* @throws Zend_Mail_Protocol_Exception
	*/
	
	public function __construct($params) 
	{

		//TODO: STILL REQUIRED ?!
		$this->log = Zend_Registry::get('log');
		
		$this->_config = $params;
		$this->_has['flags'] = true;
	
		if( isset($this->_config['namespaceName']) )
		{
			$this->_namespaceName = $this->_config['namespaceName'];
		}
		
		$this->_session = new Zend_Session_Namespace($this->_namespaceName); // session used as seperate persistent store for if caching disabled
	
		//N.B. Internal Vars are UTF-8 so en/decodeUTF7-IMAP when talking to protocol
		if( isset($this->_config['folder']) )
		{
		
			$this->_session->folderNameCurrentGlobal = $this->_config['folder']; //commonly used to open specific folder or change folder
			
		}
		elseif( isset($this->_session->folderNameCurrentGlobal) )
		{

			$this->_config['folder'] = $this->_session->folderNameCurrentGlobal;

		}
		elseif( isset($this->_config['defaultFolder']) ) 
		{
			
			$this->_session->folderNameCurrentGlobal = $this->_config['defaultFolder'];
			$this->_config['folder'] = $this->_config['defaultFolder']; //_config not persistent but useful in other classes
			
		}
		//at this point either $this->_config['folder'] will have the default userConfig logon folder, or be null in which case class will open default folder
	
		if( is_array($params) )
		{
			
			$params = (object)$params;
			
		}
	
		if( $params instanceof Atmail_Mail_Protocol_Imap )
		{
			
			$this->_protocol = $params;
			
		}
	
		$this->updateCacheStamp = time();
	
		if( isset($this->_session->_folders) )
		{
			$this->_folders = &$this->_session->_folders;
		}
		
		if( isset($this->_config['sortMaxLimit']) )
		{
		 
			$this->_sortMaxLimit = $this->_config['sortMaxLimit'];
			
		}
		
		if( isset($this->_config['threadingMaxLimit']) )
		{
		 
			$this->_threadingMaxLimit = $this->_config['threadingMaxLimit'];
			
		}
		 
		if( isset($this->_config['threadsLimitMonths']) )
		{
		 
			$this->_threadsLimitMonths = $this->_config['threadsLimitMonths'];
			
		}
		 
		if( isset($this->_config['enableMessagePreview']) )
		{
		 
			$this->_enableMessagePreview = $this->_config['enableMessagePreview'];
			
		}
	}
	
	public function connect()
	{	
		if( $this->_connected )
			return $this->_folderStat;
		
		// Make the connection as active, to avoid connecting over and over
		$this->_connected = true;
		
		//config folder (if set - like in a user folder change request or default logon folder) overides session folder
		if( !isset($this->_config['folder']) && isset($this->_session->folderNameCurrentGlobal) )
			$this->_config['folder'] = $this->_session->folderNameCurrentGlobal;
		
		if( !empty($this->_config['folder']) && $this->_config['folder'] == 'undefined' )
			$this->_config['folder'] = $this->getMainFolderName();
		
		$this->_messagesHeadersFetched = false;
		$this->_has['flags'] = true;
		if( !isset($this->_config['user']) ) 
			throw new Atmail_Mail_Storage_Exception('Need at least user in params');
		
		$host	 = isset($this->_config['host'])	 ? $this->_config['host']	 : 'localhost';
		$password = isset($this->_config['password']) ? $this->_config['password'] : '';
		$port	 = isset($this->_config['port']) && strlen($this->_config['port'])>0 ? $this->_config['port']	 : null;
		$ssl	  = isset($this->_config['ssl'])	  ? $this->_config['ssl']	  : false;

		$this->_protocol = new Atmail_Mail_Protocol_Imap();
		Zend_Registry::get('log')->imap('About to try connect to: ' . $host . ':' . $port . ':' . $ssl );
		$this->_protocol->connect($host, $port, $ssl);
		if( !$this->_protocol->login($this->_config['user'], $password) ) 
		{
			$this->_connected = false;
			throw new Atmail_Exception('Login failed - Username or password is incorrect');
			
		}
		
		if( isset($this->_session->_protocolCapability) )
			$this->_protocol->setCapability($this->_session->_protocolCapability);
		else
			$this->_session->_protocolCapability = $this->getCapability();
		
		if( isset($this->_session->_protocolNamespaces) )
		   $this->_protocol->setNamespaces($this->_session->_protocolNamespaces);
		else
			$this->_session->_protocolNamespaces = $this->_protocol->getNamespaces();
				
		//check for minimal set of folders
		if( !isset($this->_session->requiredFoldersChecked) || $this->_session->requiredFoldersChecked == false )
		{
			$requiredFolders = array(
				$this->getMainFolderName(), 
				$this->getMainFolderName('Sent'), 
				$this->getMainFolderName('Trash'), 
				$this->getMainFolderName('Drafts'), 
				$this->getMainFolderName('Spam') 
			);
			
			$existingFolders = new RecursiveIteratorIterator($this->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
			
			//by now we have calculated delimiter
			foreach( $requiredFolders as $requiredFolder )	
			{
				$found = false;
				foreach( $existingFolders as $existingFolder)
				{
					
					if( $requiredFolder == $existingFolder->getGlobalName())
					{
						$found = true;
						break;
					}
					
				}   
				if( !$found )
				{
					
					try
					{
						$folderNameGlobal = $this->createFolder( $requiredFolder, null, true );
					}
					catch (Exception $e)
					{
						//ignore "folder already exists" just in case sace insensitive server or something (not a fatal error)
					}
				}
				
			}
			$this->_session->requiredFoldersChecked = true;
		}
				
		$folder = ( isset($this->_config['folder']) ? $this->_config['folder'] : $this->getMainFolderName() );
        try
        {
		$this->_folderStat = $this->selectFolder( $folder );
        }
        catch (Exception $e)
        {
            $this->_folderStat = $this->selectFolder( $this->getMainFolderName() );
        }
	
		$this->_session->folderNameCurrentGlobal = $this->getCurrentFolder();
		
		// Return the message folder select stat ( total, new, unread )
		return $this->_folderStat;
	}
	
	public function getCapability() 
	{
		
		$this->connect();
		$capa = $this->_protocol->getCapability();
		if (is_array($capa[0]))
		{
			return $capa[0];
		}
		
		return $capa;
	}
	
	public function capabilitySupported( $string )
	{
		
		foreach( $this->getCapability() as $capabilityString ) 
		{
			
			if( strpos(strtoupper($capabilityString), strtoupper($string)) !== false )
				return true;
			
		}   
		return false;
		
	}
	
	public function getSortMaxLimit() 
	{
		
		return $this->_sortMaxLimit;
		
	}
	
	public function getThreadingMaxLimit() 
	{
		
		return $this->_threadingMaxLimit;
		
	}
	
	public function threadsSupported()
	{
		
		foreach( $this->getCapability() as $capabilityString ) 
		{
			$compat[strtoupper($capabilityString)] = 1;
		}

		// Decide which thread sorting algortihm is available ( THREAD=REFS preferred )
		if( !empty($compat['THREAD=REFS']) ) {
			$this->_threadType = 'REFS';
			return true;
		} elseif( !empty($compat['THREAD=REFERENCES']) ) {
			$this->_threadType = 'REFERENCES';
			return true;
		} elseif( !empty($compat['THREAD']) ) {
			$this->_threadType = 'REFERENCES';
			return true;
		}

		return false;
	
	}
	
	public function setThreadsEnabled( $state = true )
	{
	
		if( $state == 0 || $state == 'false' || $state === false )
		{
			
			$this->_threadsEnabled = false;
			return $this->_threadsEnabled;
		
		}	
		   	
		if( !$this->threadsSupported() )
		{
			
			$this->_threadsEnabled = false;
			return $this->_threadsEnabled;
			
		}
		
		if( $this->_threadsEnabled == false )
		{
			
			return $this->_threadsEnabled;
			
		}
		
		//finally if passed all the other rules then can set true - and is already true
		return $this->_threadsEnabled;
					
	}	
	
	public function getThreadsEnabled()
	{
		
		return $this->_threadsEnabled;
		
	}

	public function close()
	{
		if( !$this->_connected )
		{
			return;
		}
		
		$this->_currentFolder = '';
		$this->_protocol->logout();
	}


	// MAGIC FUNCTIONS
	////////////////////////////////////////////////////////////////////////////////
	
	public function current() //overloaded the class iterator to handle overloading getMessage to use uniqueId
	{
		return $this->getMessage( $this->getUniqueId($this->_iterationPos) );
	}

	/**
	* returns array of recent/unreadmessages
	* consider argument to specify total on diff criteria. i.e. filter date, flags, unread/total
	*/
	
	public function countMessages($specificFolder = null)
	{
		
		if( $specificFolder == null )
			$specificFolder = $this->getCurrentFolder();
		
		$folderInfo = $this->getFolderInfo( $specificFolder );
		
		return $folderInfo;
		
	}
	
/*
	public function countUnseenMessages( $folderNameGlobal = null )
	{

		if( $folderNameGlobal == null )
		{
			
			$folderNameGlobal = $this->getCurrentFolder();
			
		}

		$response = $this->_protocol->requestAndResponse('STATUS', array('"' . $folderNameGlobal . '" (UNSEEN)'), true);

		if( !$response )
		{
			
			return 0;
			
		}   
		
		//get the unseen count from the response
		$responsePart = substr($response[0], strpos($response[0],'(')+1, strpos($response[0],')')-strpos($response[0],'(')-1 );
		$unseenCount = substr( $responsePart, strrpos($responsePart,' ')+1);
		
		return $unseenCount;
		
	}
*/
	public function getFolderUnreadCount($folder = 'INBOX'){
		$this->_protocol->examine($folder);
		return count($this->_protocol->search(array('UNSEEN')));
	}

	public function countTotalMessages()
	{
		
		$folderInfo = $this->getFolderInfo();
		return $folderInfo['exists'];
		
	}

	public function countTotalThreadMessages()
	{
		
		if( $this->getThreadsEnabled() && isset($this->_threads) )
		{
			
			return count($this->_threads);
			
		}
		else
		{
			
			return $this->countTotalMessages();
			
		}   
		
	}

	public function &getFreshUniqueIds()
	{
		
		$this->_messages = array();
		$this->connect();
		$tag = '';
		//CONSIDER: NOT in this mothod but in general: consider clever sorting like if requesting flip of list < pageVolume then just tell browser to flip existing result
		// obviosuly caching would need to retain integrity and session sort order vals   
		if( $this->_sort == null || $this->_sort == 'UID' || !in_array('SORT', $this->getCapability() ) )
		{
		
			$this->_sort = 'UID';
			
			//this will result in an order of ARRIVAL ASC, we doing to default to DESC so will flip it
			$this->_protocol->sendRequest('SEARCH', array('ALL'), $tag);
			$response = $this->_protocol->readResponse($tag);

			if(is_array($response) && isset($response[0]) && $response[0][0] == 'SEARCH')
			{
				$response = $response[0];
				$response = array_slice($response, 1);	// remove 'SEARCH' from array
			}
			else
			{
				$response = $this->_protocol->fetch(array('UID'), 1, INF);
			}

			foreach( $response as $UID )
			{
				$this->_messages[] = array('UID' => $UID);
			}
			if( is_array($this->_messages) && $this->_order == 'REVERSE' )
			{
				$this->_messages = array_reverse($this->_messages);
			}
		}
		else
		{
			$sortOrderString = trim($this->_order . ' ' . $this->_sort);
			$this->_protocol->sendRequest('SORT', array('(' . $sortOrderString . ') us-ascii ALL'), $tag);
			$response = $this->_protocol->readResponse($tag);
			if( $response == false )
			{
				throw new Exception('Error returned from server while fetching list of messages: ' . $this->_protocol->_previousCommand);
			}
			
			foreach( $response[0] as $UID )
			{
				//skip periferal server responses
				if($UID == 'SORT')
				{
					continue;
				}
				$this->_messages[] = array('UID' => $UID);
			}
		}
		$this->dataSource = 'fresh';
		$this->_updateCacheStamp = time();
		
		//take the opportunity to try force threadsEnabled to false if too many messages
		if( count($this->_messages) > $this->getThreadingMaxLimit() )
		{
			$this->setThreadsEnabled( false );
		}
	
		return $this->_messages;
	}

	public function &getUniqueIds()
	{
		if( $this->_messages === null )
		{
			$this->getFreshUniqueIds();
		}
		return $this->_messages;
	}

	/**
	* overloaded primarily to enable updating of current cache data on success
	*/
	
	public function copyMessage($idOrArrayOfIds, $folder)
	{
		//TODO: when upgrading Storage/Imap to handle multiple folders(entire imap object) in one cache then upgrad this top copy nodes into
		return parent::copyMessage($idOrArrayOfIds, $folder );
	}
	
	/**
	* overloaded primarily to enable updating of current cache data on success
	*/
	
	public function removeMessage($idOrArrayOfIds)
	{
		//TODO: update cached data by removing nodes out of _messages and _threads
		//recursivelyRemoveUIDFromThreadNode
		if( !is_array($idOrArrayOfIds) )
		{
			$idOrArrayOfIds = array($idOrArrayOfIds);
		}
		foreach( $idOrArrayOfIds as $UID )
		{
			$this->removeMessageFromMessages($UID);
	
			for( $a = 0 , $_threadsC = count($this->_threads) ; $a < $_threadsC ; $a++ )
			{
				$this->recursivelyRemoveMessageFromThreads($this->_threads[$a], $UID);
				if( $this->_threads[$a]['UID'] == null && count($this->_threads[$a]['_children']) == 0 )
				{
					$result = array_splice($this->_threads, $a, 1);
				}
			}
		}
	
		return parent::removeMessage($idOrArrayOfIds);
	}
	
	private function removeMessageFromMessages($UID)
	{

		for( $a = 0 , $_messageC = count($this->_messages) ; $a < $_messageC ; $a++ )
		{

			if( $this->_messages[$a]['UID'] == $UID )
			{

				unset($this->_messages[$a]);
				break;

			}

		}

	}
	
	private function recursivelyRemoveMessageFromThreads(&$threadsNode, $UID)
	{
		if( $threadsNode['UID'] == $UID )
		{
			$threadsNode['UID'] = null;
		}
		
		// TODO: trivial cleanup of old data like _headers, totals etc.
		for( $a = 0 , $_childrenC = count($threadsNode['_children']) ; $a < $_childrenC ; $a++ )
		{
			$this->recursivelyRemoveMessageFromThreads($threadsNode['_children'][$a], $UID);
			if( $threadsNode['_children'][$a]['UID'] == null && count($threadsNode['_children'][$a]['_children']) == 0 )
			{
				array_splice($threadsNode['_children'], $a, 1);
			}
		}
		//TODO: clean up index keys for removed children
	}
	
	// THREAD FUNCTIONS
	////////////////////////////////////////////////////////////////////////////////
	
	public function &getThreads()
	{
		if( $this->_threads == null )
			$this->getFreshThreads();
		
		//TODO: check sort and order = means we will need to store threads last sort and order for decision
		return $this->_threads;
	}
	
	public function massageThreads($threads, $depth = 0)
	{
		if($depth > 4) return $threads;
		
		$finalThreads = array();
		
		foreach ($threads as $k => $v)
		{
			if ($v['UID'] == 0)
			{
				$finalThreads = array_merge($finalThreads, $this->massageThreads($v['_children'], $depth++));
			}
			else
			{
				$finalThreads[] = $v;
			}
		}
		
		return $finalThreads;
	}

	public function &getFreshThreads()
	{
		
		//TODO: if !Capability Threads just return flat _messages as the threads array (same except no _sibling_messages array)
		$folderInfo = $this->getFolderInfo(); 
		if( $folderInfo['exists'] > $this->getThreadingMaxLimit() )
		{
			$this->_threads = array();
			return $this->_threads;
		}
	
		//$a1 = microtime(true);
		if( $this->_threadsLimitMonths == 0 ) 
			$cutOffDateString = 'ALL';
		else
			$cutOffDateString = 'SINCE ' . strftime("%d-%b-%Y", strtotime('-' . $this->_threadsLimitMonths . ' Months') );
		
		try
		{
		
			$response = $this->_protocol->requestAndResponse('UID THREAD', array($this->_threadType . ' us-ascii ' . $cutOffDateString), true);
		
		}
		catch(Exception $e)
		{
		
			throw new Exception('Server error while trying to fetch threads.');//': ' . $->getMessage() );//$this->_protocol->_previousCommand);
		
		}
		$intermediate = substr($response[0], 7);
		$this->_threads = $this->_decodeThreadsResponse($intermediate);
        // massage any orphaned thread uids
		$this->_threads = $this->massageThreads($this->_threads);

		//$timeNow = microtime(true);
		$this->_sortUnsortedThreads();
		//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": time to _sortUnsortedThreads(): " . round(microtime(true)-$timeNow, 2));
		//$timeNow = microtime(true);
		$this->updateCacheStamp = time();
		
		return $this->_threads;
	
	}
	
	private function _sortUnsortedThreads()
	{
		
		if ( $this->_threads == null || !is_array($this->_threads) )
			return;
        if( $this->_sort == null || $this->_sort == 'ARRIVAL' || $this->_sort == 'UID' )
		{

			usort($this->_threads, array($this, '_sortBy_latestUID'));
			if( $this->_order == 'REVERSE' )
				$this->_threads = array_reverse($this->_threads);

		}
		else
		{

			$threadsC = count($this->_threads);
			if ( $threadsC > $this->getThreadingMaxLimit() ) //failsafe
				return;
	
			if ( $threadsC > $this->getSortMaxLimit() ) //dont sort further if over limit
				return;
	
			$this->getUniqueIds(); //fetch list of messages (sorted) to sort threads against getUniqueIds will already be order by order
	
			//this will sort in the order of _messages current state so no order flipping required.
			$sortedThreads = array();
			foreach( $this->_messages as $message )
			{
				//go through the order of _messages and compose new array of $this->_threads that were found in order
				foreach( $this->_threads as $k => $thread )
				{
					if( $message['UID'] == $thread['_latestUID'] )
					{
						$sortedThreads[] = $thread;
						array_splice($this->_threads,$k,1);
						$threadsC--;
						break;
					}
				}
				if( $threadsC == 0 )
					break;
			}
			$this->_threads = $sortedThreads;
			
		}   
		
	}
	
	private function _sortBy_latestUID($a,$b)
	{

		$sortable = array($a['_latestUID'],$b['_latestUID']);
		$sorted = $sortable;
		sort($sorted);
		return ($sorted[0] == $sortable[0]) ? -1 : 1;

	}
	
	
	/**
	* decodes a THREAD response into a nested array similar to _messages with ['UID'] and the addition of ['siblings']
	*/
	private function _decodeThreadsResponse(&$threadsResponseString, &$currentStringPos = -1, $_lastOpenBracketDepth = 0, $_currentDepth = 0)
	{
		$threadsResponseStringC = strlen($threadsResponseString);
		//TODO: compose/assemble thread participants names from recipient addresses.
		$thisNode = array('UID' => null, '_children' => array(), '_totalChildren' => 0, '_threadParticipantNames' => null, '_latestUID' => null );
		$childrenK = -1;
	
		while( ++$currentStringPos < $threadsResponseStringC )
		{
			$nextClosingBracketPos = strpos($threadsResponseString, ')', $currentStringPos);
			$nextSpacePos = strpos($threadsResponseString, ' ', $currentStringPos);
			$nextOpeningBracketPos = strpos($threadsResponseString, '(', $currentStringPos);
			$nextSpaceOpeningBracketPos = strpos($threadsResponseString, ' (', $currentStringPos);
	
			$firstMatchingPos = $threadsResponseStringC;
			if( $nextSpacePos !== false &&  $nextSpacePos < $firstMatchingPos)
			{
				$nextState = 1; //space
				$firstMatchingPos = $nextSpacePos;
			}
	
			if( $nextOpeningBracketPos !== false && $nextOpeningBracketPos < $firstMatchingPos )
			{
				$nextState = 2; // opening bracket
				$firstMatchingPos = $nextOpeningBracketPos;
			}
	
			if( $nextSpaceOpeningBracketPos !== false && $nextSpaceOpeningBracketPos <= $firstMatchingPos)
			{
				$nextState = 3; // space with an opening bracket
				$firstMatchingPos = $nextSpaceOpeningBracketPos;
			}
	
			if( $nextClosingBracketPos !== false &&  $nextClosingBracketPos < $firstMatchingPos )
			{
				$nextState = 4; //closing bracket
				$firstMatchingPos = $nextClosingBracketPos;
			}

			//if we at the end
			if( $firstMatchingPos == $threadsResponseStringC )
			{
				$nextState = 5;
			}
	
	
			if( $firstMatchingPos - $currentStringPos > 0 )
			{
				$thisNode['UID'] = substr($threadsResponseString, $currentStringPos, $firstMatchingPos - $currentStringPos);
				$thisNode['_totalChildren']++;
				$thisNode['_latestUID'] = $thisNode['UID'];
			}
	
			if( $nextState == 1 )
			{	
				//space so recurse
				$currentStringPos = $firstMatchingPos;
				$thisNode['_children'][++$childrenK] = $this->_decodeThreadsResponse($threadsResponseString, $currentStringPos, $_lastOpenBracketDepth, $_currentDepth+1);
				$thisNode['_totalChildren'] += $thisNode['_children'][$childrenK]['_totalChildren'];
	
				if( $thisNode['_latestUID'] < $thisNode['_children'][$childrenK]['_latestUID'] )
				{
					$thisNode['_latestUID'] = $thisNode['_children'][$childrenK]['_latestUID'];
				}
	
				if($_currentDepth > $_lastOpenBracketDepth)
				{
					return $thisNode;
				}
			} 
			elseif( $nextState == 2 ) 
			{ 
				//open bracket
				$_lastOpenBracketDepth = $_currentDepth;
				$currentStringPos = $firstMatchingPos;
				$thisNode['_children'][++$childrenK] = $this->_decodeThreadsResponse($threadsResponseString, $currentStringPos, $_lastOpenBracketDepth, $_currentDepth+1);
				$thisNode['_totalChildren'] += $thisNode['_children'][$childrenK]['_totalChildren'];
	
				if( $thisNode['_latestUID'] < $thisNode['_children'][$childrenK]['_latestUID'] )
				{
					$thisNode['_latestUID'] = $thisNode['_children'][$childrenK]['_latestUID'];
				}
				if($_currentDepth > $_lastOpenBracketDepth)
				{
					return $thisNode;
				}
			} 
			elseif( $nextState == 3 )
			{ 
				//space and open bracket
				$currentStringPos = $firstMatchingPos+1;//extra char (2 char match)
				$_lastOpenBracketDepth = $_currentDepth;
				$thisNode['_children'][++$childrenK] = $this->_decodeThreadsResponse($threadsResponseString, $currentStringPos, $_lastOpenBracketDepth, $_currentDepth+1);
				$thisNode['_totalChildren'] += $thisNode['_children'][$childrenK]['_totalChildren'];
	
				if( $thisNode['_latestUID'] < $thisNode['_children'][$childrenK]['_latestUID'] )
				{
					$thisNode['_latestUID'] = $thisNode['_children'][$childrenK]['_latestUID'];
				}
	
				if($_currentDepth > $_lastOpenBracketDepth)
				{
					return $thisNode;
				}
			} 
			elseif( $nextState == 4 )
			{
				$currentStringPos = $firstMatchingPos;
				return $thisNode;
			} 
			elseif( $nextState == 5 ) 
			{ 
				//possible bug could require only returning _children on first iteration
				$currentStringPos = $firstMatchingPos;
				return $thisNode['_children'];
			}
		}
		return $thisNode['_children'];
	}
	
	/**
	* returns _threads with BasicHeaders populated
	* $depth specifies how deep to recurse fetching header info
	* @param Integer $limit Number of months to search back for threads 
	*/
	public function getThreadsBasicHeadersPage($pageNumber, $pageVolume)
	{
		
		$folderInfo = $this->getFolderInfo(); //current folder ( will also update threadsEnabled)
		if( !$this->getThreadsEnabled() )
		{
			
			//produce list of ordinal ids in reverse order
			$relevantMessageOrdinalIDs = array();
			if( $this->_order == 'REVERSE' )
			{
				$highestOrdinalId = $folderInfo['exists'] - $pageVolume*($pageNumber-1);
				$lowestOrdinalId =  $folderInfo['exists'] - ($pageVolume*$pageNumber)+1;
			}
			else
			{
				$highestOrdinalId = $pageVolume*$pageNumber;
				$lowestOrdinalId = ($pageVolume*($pageNumber-1))+1;
			}
			if( $lowestOrdinalId < 1 )
				$lowestOrdinalId = 1;

			$relevantMessages = $this->getBasicHeaders( $lowestOrdinalId, $highestOrdinalId, true );
			if( $this->_order == 'REVERSE' )
				$relevantMessages = array_reverse( $relevantMessages );
	
			return $relevantMessages;

		}
		if( $this->_threads == false )
			$this->getThreads();
		
		//calculate which thread UIDs we going to fetch (i.e. page)
		$pages = array_chunk($this->_threads, $pageVolume);
		$pageLines = &$pages[$pageNumber-1];
		
		$pageLinesC = count($pageLines);
		$relevantMessageUIDs = array();
	
		for( $a = 0 ; $a < $pageLinesC ; $a++ )
			$relevantMessageUIDs[] = $pageLines[$a]['_latestUID'];
	
		$relevantMessages = $this->getBasicHeaders( $relevantMessageUIDs );
		$relevantMessagesC = count( $relevantMessages );
	
		for( $a = 0 ; $a < $pageLinesC ; $a++ )
		{

			$latestChild = &$this->findLatestThreadChild($pageLines[$a]);
			if( $latestChild == null )
				$latestChild = &$pageLines[$a];

			for( $b = 0 ; $b < $relevantMessagesC ; $b++ )
			{
				if( $latestChild['UID'] == $relevantMessages[$b]['UID'] )
				{
					$latestChild = array_merge($latestChild, $relevantMessages[$b]);
					break;
				}
			}
		}
		//update $this->_messages incase used elsewhere, e.g. cached
		if( $this->_messages == null )
			$this->_messages = $relevantMessages;
		else
		{
		
			foreach( $relevantMessages as $message )
			{
		
				foreach( $this->_messages as $k => $_message )
				{
		
					if( $message['UID'] == $_message['UID'] )
					{
		
						$this->_messages[$k] = array_merge($this->_messages[$k], $message);
						break;
		
					}
		
				}
		
			}
		
		}
		return $pageLines;
		
	}
	
	public function getThreadWithAllBasicHeaders(&$threadNode)
	{

		$allThreadUIDs = $this->recursivelyGetAllThreadUIDs($threadNode);

		$basicHeaders = $this->getBasicHeaders($allThreadUIDs);
		$this->_recurseBasicHeadersIntoThread($threadNode, $basicHeaders);

	}
	
	public function recursivelyGetAllThreadUIDs(&$threadNode)
	{
		$threadUIDs = array();
		if( isset( $threadNode['UID']) )
		{
			$threadUIDs[] = $threadNode['UID'];
		}
		if( isset($threadNode['_children']) )
		{
			foreach( $threadNode['_children'] as $k => $v )
			{
				$threadUIDs = array_merge( $threadUIDs, $this->recursivelyGetAllThreadUIDs($threadNode['_children'][$k]) );
			}
		}
		return $threadUIDs;
	}

	private function _recurseBasicHeadersIntoThread(&$threadNode, &$basicHeadersArray)
	{
		if( array_key_exists( 'UID', $threadNode ) )
		{
			foreach($basicHeadersArray as $basicHeader )
			{
				if( $basicHeader['UID'] == $threadNode['UID'] )
				{
					$threadNode = array_merge($threadNode, $basicHeader);
				}
			}
		}
		foreach( $threadNode['_children'] as $k => $child )
		{
			$this->_recurseBasicHeadersIntoThread($threadNode['_children'][$k], $basicHeadersArray);
		}
	}
	
	public function &findLatestThreadChild(&$threadNode)
	{
		//in its simplest form = highest uid = last uid processed
		//try find last node
		//TODO: confirm that under all circumstances this will return latest (what about older section of thread with a more recent reply.)
		$latestChildNode = null;
		if( count($threadNode['_children']) > 0 )
		{
			foreach( $threadNode['_children'] as $k => $thread )
			{
				$returnedLatestThreadChild = &$this->findLatestThreadChild($threadNode['_children'][$k]);
			
				if( $latestChildNode == null || ($returnedLatestThreadChild['UID'] > $latestChildNode['UID']) )
				{
					$latestChildNode = &$returnedLatestThreadChild;
				}
			}
		}
		if(
		( $latestChildNode == null && isset($threadNode['UID']) && $threadNode['UID'] != '' ) ||
		( isset($threadNode['UID']) && $threadNode['UID'] > $latestChildNode['UID'] ) ) 
		{
			$latestChildNode = &$threadNode;
		}
		return $latestChildNode;
	}

	public function combineThreadFlags(&$threadNode, $defaults = array('totals' => array(), Zend_Mail_Storage::FLAG_ANSWERED => true, Zend_Mail_Storage::FLAG_SEEN => true) )
	{
		$combinedFlags = $defaults;
		// check current thread
		if(!array_key_exists('FLAGS', $threadNode) && $threadNode['UID'] != 0)
		{
			$threadNode['FLAGS'] = $this->getFlags( $threadNode['UID'] );
		}

		$threadNodeFlags = $threadNode['FLAGS'];

		foreach($threadNodeFlags as $k => $v)
		{
			if(!array_key_exists($k, $combinedFlags['totals']))
			{
				$combinedFlags['totals'][$k] = 0;
			}
			$combinedFlags['totals'][$k]++;
		}

		if(		!array_key_exists(Zend_Mail_Storage::FLAG_ANSWERED, $threadNodeFlags)
			&& 	array_key_exists(Zend_Mail_Storage::FLAG_ANSWERED, $combinedFlags))
		{
			unset($combinedFlags[Zend_Mail_Storage::FLAG_ANSWERED]);
		}

		if(	!array_key_exists(Zend_Mail_Storage::FLAG_SEEN, $threadNodeFlags)
			&&	array_key_exists(Zend_Mail_Storage::FLAG_SEEN, $combinedFlags))
		{
			unset($combinedFlags[Zend_Mail_Storage::FLAG_SEEN]);
		}

		if(array_key_exists(Zend_Mail_Storage::FLAG_FLAGGED, $threadNodeFlags))
		{
			$combinedFlags[Zend_Mail_Storage::FLAG_FLAGGED] = true;
		}

		// see if current thread node has any children
		if( array_key_exists('_children', $threadNode) )
		{
			foreach($threadNode['_children'] as &$childThread)
			{
				$combinedFlags = $this->combineThreadFlags($childThread, $combinedFlags);
			}
		}
		return $combinedFlags;
	}

	/**
	* efficiently returns page of basic headers (using minimal IMAP commands, for performance)
	* assumes $this->sort and order already set
	*
	*/
	public function getBasicHeadersPage($pageNumber, $pageVolume)
	{
		
		$relevantMessageOrdinalIDs = array();
		
		//if more than x messages then simply fetch and sort on id
		//else get list of ids to fetch from _messages
		$messagesCount = count($this->_messages);  
		
		//TODO: populate folderInfo on SELECT not just EXAMINE	
		if( $this->_order == 'REVERSE' )
		{
			$highestOrdinalId = $messagesCount - $pageVolume*($pageNumber-1);
			$lowestOrdinalId =  $messagesCount - ($pageVolume*$pageNumber)+1;
		}
		else
		{
			$highestOrdinalId = $pageVolume*$pageNumber;
			$lowestOrdinalId = ($pageVolume*($pageNumber-1))+1;
		}
		
		//sanity check
		if( $lowestOrdinalId < 1 )
		{
			$lowestOrdinalId = 1;
		}
		if( $highestOrdinalId > $messagesCount )
		{
			$highestOrdinalId = $messagesCount;
		}
		if( $messagesCount < $this->getSortMaxLimit() )
		{

			//compile list of messages in the order to fetch them
			$messageUIDs = $this->getUniqueIds();
			$messageUIDsC = count($messageUIDs);
			$relevantUIDs = array();
			for( $a = ($pageVolume*($pageNumber-1)) ; $a < ($pageVolume*$pageNumber) ; $a++ )
			{
				if( $a  >= $messageUIDsC )
					break;
				$relevantUIDs[] =  $messageUIDs[$a]['UID'];
			}
			
			$relevantMessages = $this->getBasicHeaders( $relevantUIDs );
		} 
		else
		{
			
			if( $this->_order == 'REVERSE' )
			{
				$highestOrdinalId = $messagesCount - $pageVolume*($pageNumber-1);
				$lowestOrdinalId =  $messagesCount - ($pageVolume*$pageNumber)+1;
			}
			else
			{
				$highestOrdinalId = $pageVolume*$pageNumber;
				$lowestOrdinalId = ($pageVolume*($pageNumber-1))+1;
			}
			
			//sanity check
			if( $lowestOrdinalId < 1 )
				$lowestOrdinalId = 1;
			if( $highestOrdinalId > $messagesCount )
				$highestOrdinalId = $messagesCount;

			$relevantMessages = $this->getBasicHeaders( $lowestOrdinalId, $highestOrdinalId, true );
		
			if( $this->_order == 'REVERSE' )
				$relevantMessages = array_reverse( $relevantMessages );

		}
		return $relevantMessages;

	}
	
	public function getQuota( $namespace = "INBOX" )
	{
		
		$this->connect();
		return $this->_protocol->getQuota($namespace);
		
	}
	
	public function getSpecificHeaders( $UIDArray, $headerFields )
	{
		
		//force connect to server if didnt need to yet
		$this->connect();
		if( is_array($UIDArray[0]) && array_key_exists('UID', $UIDArray[0]) )
		{
			$simpleUIDArray = array();
			foreach($UIDArray as $v )
				$simpleUIDArray[] = $v['UID'];
		}
		else
			$simpleUIDArray = &$UIDArray;
		//$results = $this->_protocol->nestedFetch($headerFields, $simpleUIDArray);
		$results = $this->_protocol->fastFetch( $headerFields, $simpleUIDArray  );
		return $results;
		
	}
	
	/**
	* get unique id for one or all messages
	*
	* if storage does not support unique ids it's the same as the message number
	*
	* @param int|null $id message number
	* @return array|string message number for given message or all messages as array
	* @throws Zend_Mail_Storage_Exception
	*/
	public function getBasicHeaders($startUIDPos = null, $endUIDPos = null, $forceOrdinalIDs = false)
	{
		$_PREVIEW_REQUIRED_SIZE = 10; // at least one word.
		$_PEEK_READ_SIZE = 1024; // 1kb default peek
		$_PEEK_READ_RETRY_MULTIPLY = 3;
		$_PEEK_READ_SIZE_MAXIMUM = 1024 * 50; // 50kb walk should be more then enough

		//obviously this must obey current sort criteria
		//this means either fetching plain list (sorted naturally by INTERNALDATE), or specifying the UIDS in order
		//also must only fetch basic headers for page requested to minimise load on server and client
		//$this->_messagesBasicHeadersFetched will only be set to true if 1:*
		if( is_array($startUIDPos) && count($startUIDPos) && $endUIDPos == null )
		{
			//nothing to do here, ready to passthrough. logic in this order for ease of reading/maintainance
			if( isset($startUIDPos[0]) && is_array($startUIDPos[0]) && array_key_exists('UID', $startUIDPos[0]) )
			{
				//convert into simple array of uids
				$simpleStartUIDPos = array();
				foreach($startUIDPos as $v )
				{
					$simpleStartUIDPos[] = $v['UID'];
				}
				$startUIDPos = $simpleStartUIDPos;
			}
		}
		elseif( ($startUIDPos == 1 && $endUIDPos == INF) || ($startUIDPos == null && $endUIDPos == null) )
		{
			//i.e. fetch all implied
			//if _sort set then use all of _messages['UID], else a subset
			//now compose sorted array of uids to fetch
			if( $this->_sort != null )
			{
				if( $this->_messages == null )
				{
					$this->getFreshUniqueIds();
				}

				$startUIDPos = array();
				$endUIDPos = null;
				foreach( $this->_messages as $_message )
				{
					$startUIDPos[] = $_message['UID'];
				}
			}
			else
			{
				$startUIDPos = 1;
				$endUIDPos = INF;
			}
			$this->_messagesBasicHeadersFetched = true;
		}
		elseif( $startUIDPos != null && $endUIDPos != null )
		{
			//nothing to do, range already specified
		}
		else
		{
			//other ranges
			//certain range of uids set $startUID may already be the range (as an array, caught above), or calculate it here
			if( $this->_messages == null )
			{
				$this->getFreshUniqueIds();
			}
			$startUIDArray = array();
			foreach( $this->_messages as $k => $_message )
			{
				if( $k >= ($startUIDPos-1) && $k >= ($endUIDPos-1) )
				{
					$startUIDArray[] = $_message['UID'];
				}
			}
			$startUIDPos = $startUIDArray;
			$endUIDPos = null;
		}

		//force connect to server if didnt need to yet
		$this->connect();

		//There are cases where different mail servers respond slightly differently meaning the processing will have to be adjusted in 2 ways:
		// 1. try modify the automata rules to handle the variances (if they can be defined legally in syntax)
		// 2. add rules during handling/processing for variances
		$mailServerIdentifier = 'unknown';
		if(
		( is_array($startUIDPos) && count($startUIDPos) == 0 )
		||
		( ($startUIDPos == '' || $startUIDPos == null) && ($endUIDPos == '' || $endUIDPos == null) )
		)
		{
			$messagesUIDs = array(); //don't bother running query if asking for no messages
		}
		else
		{
			// Calculate the unique name for the cache
			$cacheName = Atmail_Cache::generateCacheIdString(array($this->getCurrentFolder(), $startUIDPos, $endUIDPos, $forceOrdinalIDs));
			$messagesUIDs = Atmail_Cache::fetch(Atmail_Cache::TYPE_MAIL_UIDS, $cacheName );

			if(empty($messagesUIDs))
			{
				$messagesUIDs = $this->_protocol->nestedFetch(array('UID', 'INTERNALDATE', 'RFC822.SIZE', 'FLAGS', 'BODYSTRUCTURE', 'BODY.PEEK[HEADER.FIELDS (delivery-date date subject from content-type to cc message-id importance x-priority x-msmail-priority)]'), $startUIDPos, $endUIDPos, $forceOrdinalIDs);

				// For a cached response, we need to get a list of all the message flags, since these can change and the cache will be stale
				$messageFlags = $this->_protocol->nestedFetch(array('UID', 'FLAGS'), $startUIDPos, $endUIDPos, $forceOrdinalIDs);

				$count = 0;
				foreach($messageFlags as $flag)
				{
					$messagesUIDs[$count]['FLAGS'] = $flag['FLAGS'];
					$count++;

				}

				// Write the updated cache to disk, only when we have new content
				Atmail_Cache::store(Atmail_Cache::TYPE_MAIL_UIDS, $cacheName, $messagesUIDs );
			}
			else
			{

				// For a cached response, we need to get a list of all the message flags, since these can change and the cache will be stale
				$messageFlags = $this->_protocol->nestedFetch(array('UID', 'FLAGS'), $startUIDPos, $endUIDPos, $forceOrdinalIDs);

				$count = 0;
				foreach($messageFlags as $flag)
				{
					$messagesUIDs[$count]['FLAGS'] = $flag['FLAGS'];
					$count++;

				}
			}
		}

		/*
		 *
		 *  ATTEMPT MULTIPLE FETCH OF UIDS
		 *  GROUP BY:
		 * 		CONSECUTIVE UID
		 * 		BODY PEEK LOCATION
		 *
		 */

		// ************************************ PROCESS UIDS *********************************************** //
		// process the messages to find basic structure, encoding, charset and if previous preview already cached
		$fromByte = 0;
		$toByte = $_PEEK_READ_SIZE;
		$currentFolder = $this->getCurrentFolder();
		$tempcount = count($messagesUIDs);
		for($z=0; $z<$tempcount; $z++)
		{
			$foundNodeString = null;
			$encoding = null;
			$charset = null;

			// TODO: Move this to a global function for all backends ( POP3, etc as we extend )
			// check message structure and find first body part
			$structure = &$messagesUIDs[$z]['BODYSTRUCTURE'];
			if ($this->_enableMessagePreview)
			{
				$messagesUIDs[$z]['found'] = $this->getFirstHumanReadablePartPosStringFromStructureNode($structure, $foundNodeString, $encoding, $charset);
			}
			$messagesUIDs[$z]['encoding'] = $encoding;
			$messagesUIDs[$z]['charset'] = $charset;
			$messagesUIDs[$z]['_preview'] = null;
			
			if($this->_enableMessagePreview && $messagesUIDs[$z]['found'])
			{
				// found something interesting, attempt to retrieve the cached preview
				$cacheName = Atmail_Cache::generateCacheIdString(array($currentFolder, $messagesUIDs[$z]['UID']));
				$messagesUIDs[$z]['_preview'] = Atmail_Cache::fetch(Atmail_Cache::TYPE_MAIL_PREVIEW, $cacheName );
			}

			// cache variables for future loops
			$messagesUIDs[$z]['_firstTextPart'] = ( $foundNodeString === false || $foundNodeString == '' ) ? '1' : $foundNodeString;
			$messagesUIDs[$z]['toByte'] = $toByte;
		}

		// ********************************** GROUP UIDS FOR FETCH ****************************************** //
		$groupedMessageUIDs = group_uids($messagesUIDs, true);
		$groupedMessageUIDsMapping = array();
		$groupedMessageUIDsResponse = array();
		$groupPos = 0;

		// create mapping UID > group
		// and create cache for first $_PEEK_READ_SIZE bytes. Future loops may request up to $_PEEK_READ_SIZE_MAXIMUM
		// more bytes from the server.
		
		if ($this->_enableMessagePreview)
		{
			foreach($groupedMessageUIDs as $groupUIDsArray)
			{
				$groupUIDs = $groupUIDsArray['range'];
				if(strpos($groupUIDs, ":") !== false)
				{
					list($groupStart, $groupEnd) = explode(":", $groupUIDs);
				}
				else
				{
					$groupStart = $groupEnd = $groupUIDs;
				}
				if($groupStart > $groupEnd)
				{
					$temp = $groupStart;
					$groupStart = $groupEnd;
					$groupEnd = $temp;
				}
				for($z=$groupStart; $z<=$groupEnd; $z++)
				{
					$groupedMessageUIDsMapping[$z] = $groupPos;
				}
				$groupedMessageUIDsResponse[] = $this->_protocol->requestAndResponse('FETCH', array($groupUIDs, '(UID BODY.PEEK[' . $groupUIDsArray['_firstTextPart'] . ']<' . $fromByte . '.' . $toByte . '>)'), false);
				$groupPos++;
			}
		}

		// ********************************** FINISH GROUP UIDS FOR FETCH ****************************************** //

		//process basic headers
		$messagesUIDsC = count($messagesUIDs);
		for( $a = 0 ; $a < $messagesUIDsC ; $a++ )
		{
			//rules to pick up headers (variances between dovecot and courier)
			if( isset($messagesUIDs[$a]['BODY[HEADER.FIELDS ("date" "subject" "from" "content-type" "to" "cc" "message-id" "importance" "x-priority" "x-msmail-priority")]']) )
			{
				$rawHeaders = trim( $messagesUIDs[$a]['BODY[HEADER.FIELDS ("date" "subject" "from" "content-type" "to" "cc" "message-id" "importance" "x-priority" "x-msmail-priority")]'] );
				$mailServerIdentifier = 'courier1';
			}
			elseif( isset($messagesUIDs[$a]['BODY[HEADER.FIELDS (DATE SUBJECT FROM CONTENT-TYPE TO CC MESSAGE-ID IMPORTANCE X-PRIORITY X-MSMAIL-PRIORITY)]']) )
			{
				$rawHeaders = trim( $messagesUIDs[$a]['BODY[HEADER.FIELDS (DATE SUBJECT FROM CONTENT-TYPE TO CC MESSAGE-ID IMPORTANCE X-PRIORITY X-MSMAIL-PRIORITY)]'] );
				$mailServerIdentifier = 'dovecot';
			}
			elseif( isset($messagesUIDs[$a]['BODY[HEADER.FIELDS (date subject from content-type to cc message-id importance x-priority x-msmail-priority)]']) )
			{
				$rawHeaders = trim( $messagesUIDs[$a]['BODY[HEADER.FIELDS (date subject from content-type to cc message-id importance x-priority x-msmail-priority)]'] );
				$mailServerIdentifier = 'courier2';
			}
			elseif( isset($messagesUIDs[$a]['BODY[HEADER.FIELDS (DELIVERY-DATE DATE SUBJECT FROM CONTENT-TYPE TO CC MESSAGE-ID IMPORTANCE X-PRIORITY X-MSMAIL-PRIORITY)]']) )
			{
				$rawHeaders = trim( $messagesUIDs[$a]['BODY[HEADER.FIELDS (DELIVERY-DATE DATE SUBJECT FROM CONTENT-TYPE TO CC MESSAGE-ID IMPORTANCE X-PRIORITY X-MSMAIL-PRIORITY)]'] );
				$mailServerIdentifier = 'zimbra';
			}
			else
			{
				$messageUIDsaKeys = array_keys($messagesUIDs[$a]);
				$rawHeaders = trim( $messagesUIDs[$a][$messageUIDsaKeys[5]] );
			}

			$headers = iconv_mime_decode_headers($rawHeaders, 2, 'UTF-8');

			//check that the date field is populated... if not try to find delivery-date
			if((!isset($headers['Date']) || (isset($headers['Date']) && $headers['Date'] == '')) && isset($headers['Delivery-Date']) && $headers['Delivery-Date'] != '')
			{
				$headers['Date'] = $headers['Delivery-Date'];
			}

			foreach( $headers as $name => $header )
			{

				$lower = strtolower($name);

				//ignore some headers created during processing (does foreach include new array entries or not?)
				if( $lower == 'from_name' || $lower == 'from_email' || $lower == 'to_name' || $lower == 'to_email')
				{
					continue;
				}

				//clean missing quotations around (possibly) decoded From personal part of email address
				//CONSIDER presenting from in origional form and rather cleaning composer input, or both
				if( $lower == 'from' ||  $lower == 'to' )
				{

					$processedObjects = getProcessedRecipientObjects($header, "UTF-8");
					$preparedAddressesArray = array();
					foreach( $processedObjects as $processedObject )
					{
						if( $lower == 'from' )
						{
							$headers['from_name'] = $processedObject->personalUTF8;
							$headers['from_email'] = $processedObject->address;
						}
						else if( $lower == 'to' )
						{
							$headers['to_name'] = $processedObject->personalUTF8;
							$headers['to_email'] = $processedObject->address;
						}
						$preparedAddressesArray[] = $processedObject->recipientPreparedUTF8;
					}
					$header = implode(', ', $preparedAddressesArray);
				}


				if( $lower == $name )
				{
					continue;
				}

				unset($headers[$name]);
				if( !isset($headers[$lower]) )
				{
					$headers[$lower] = $header;
					continue;
				}
				if( is_array($headers[$lower]) )
				{
					$headers[$lower][] = $header;
					continue;
				}
				else
				{
					$headers[$lower] = array($headers[$lower], $header);
				}
			}

			// Combine the flags array to make searching it easier and faster, as in ZF methods
			if(count($messagesUIDs[$a]['FLAGS']) > 0)
			{
				$messagesUIDs[$a]['FLAGS'] = array_combine($messagesUIDs[$a]['FLAGS'], $messagesUIDs[$a]['FLAGS']);
			}

			//strip new lines out of headers to similar to dovecot
			foreach( $headers as $k => $v )
			{
				$headers[$k] = $header = str_replace( array("\n","\r"), array("",""), $v);
			}
			
			//always decode subject because an encoded subject is useless to everyone
			$headers['subject'] = messageHandling::mimeEncodedDecode($headers['subject']);
			
			$messagesUIDs[$a]['_basicHeaders'] = $headers;
			//now calculate and store attachment count (total parts - text part - html part (maybe check for filenames etc.))
			$structure = &$messagesUIDs[$a]['BODYSTRUCTURE'];
			$messagesUIDs[$a]['attachmentCount'] = $this->getNumberOfAttachmentsFromStructure($structure);
			

			$fromByte = 0;
			$toByte = $_PEEK_READ_SIZE;
			$msgPreview = '';
			$peek = '';

			// load encoding, charset and firstpartfound pre-calculated values
			$encoding = $messagesUIDs[$a]['encoding'];
			$charset = $messagesUIDs[$a]['charset'];
			if($this->_enableMessagePreview && $messagesUIDs[$a]['found'] )
			{
				// we have previously checked for a preview from the cache, so lets just load that value
				$msgPreview = $messagesUIDs[$a]['_preview'];

				if(empty($msgPreview))
				{
					require_once('class.html2text.inc');

					$retryRead = true;
					$oldPreview = $msgPreview . ' ';
						
					while ( (strlen(trim($msgPreview)) < $_PREVIEW_REQUIRED_SIZE || $retryRead) && $toByte <= $_PEEK_READ_SIZE_MAXIMUM && ($msgPreview == '' || $oldPreview != $msgPreview) )
					{
						$oldPreview = $msgPreview;
						$retryRead = false;
						$responses = array();
						// check that the pre-calculated fetch is within the requested peek chunk size
						if($toByte != $messagesUIDs[$a]['toByte'])
						{
							// outside multiple peek fetch chunk size
							$responses = array($this->_protocol->requestAndResponse('FETCH', array($messagesUIDs[$a]['UID'], '(UID BODY.PEEK[' . $messagesUIDs[$a]['_firstTextPart'] . ']<' . $fromByte . '.' . $toByte . '>)'), false));
							$responses = $responses[0];
							$messagesUIDs[$a]['toByte'] = -1;
						}
						else
						{
							// pre-calculated response available, use it!
							$responses = $groupedMessageUIDsResponse[$groupedMessageUIDsMapping[$messagesUIDs[$a]['UID']]];
						}
												
						// find the UID in the group response
						if(count($responses) == 1)
						{
							// only one response, assume its a match
							$response = $responses;
						}
						else
						{
							// multiple responses, search for the uid
							foreach($responses as $testResponse)
							{
								// TEST FOR CORRECT STRUCTURE HERE
								if($testResponse[2][0] != 'UID')
								{
									throw new Exception("Unable to find UID");
								}

								if($testResponse[2][1] == $messagesUIDs[$a]['UID'])
								{
									$response = array($testResponse);
									break;
								}
							}
						}

						if( $mailServerIdentifier == 'courier2' && isset($response[0][2][5]) )
						{
							$peek .= $response[0][2][5];
						}
						elseif( isset($response[0][2][3]) )
						{
							$peek .= $response[0][2][3];
						}
							
						$msgPreview = $this->_decodeContent($peek, $encoding, $charset);
						
						//strip <title>?</title> out cos normally a copy of subject
						if( stripos($msgPreview, '<title>') !== false )
						{
							//try find ending </title> else cucle again to append more content
							if( stripos($msgPreview, '</title>') === false )
							{
								$fromByte += $toByte;
								$toByte *= $_PEEK_READ_RETRY_MULTIPLY;
								$retryRead = true;
								continue;
							}
							else
							{
								$msgPreview = substr( $msgPreview, 0, stripos($msgPreview, '<title>') ) . substr( $msgPreview, (stripos($msgPreview, '</title>')+strlen('</title>')) );
							}
						}
						//strip "<style "|"<style>"?</style> out
						if( stripos($msgPreview, '<style ') !== false || stripos($msgPreview, '<style>') !== false )
						{
							//try find ending </title> else cucle again to append more content
							if( stripos($msgPreview, '</style>') === false )
							{
								$fromByte += $toByte;
								$toByte *= $_PEEK_READ_RETRY_MULTIPLY;
								$retryRead = true;								
								continue;
							}
							else
							{
								$msgPreview = substr( $msgPreview, 0, stripos($msgPreview, '<style>') ) . substr( $msgPreview, (stripos($msgPreview, '</style>')+strlen('</style>')) );
							}
						}
						$html2text = new html2text($msgPreview);
						$msgPreview = $html2text->get_text();

						// we need to strip if over 130 chars for preview
						if(strlen($msgPreview) > 130)
						{
							$msgPreview = mb_substr($msgPreview, 0, 130) . '&hellip;';
						}
						$messagesUIDs[$a]['_preview'] = trim($msgPreview);
						$fromByte += $toByte;
						$toByte += $toByte;
					}

					// Write the message cache, only once we have new content
					$cacheName = Atmail_Cache::generateCacheIdString(array($currentFolder, $messagesUIDs[$a]['UID']));
					Atmail_Cache::store(Atmail_Cache::TYPE_MAIL_PREVIEW, $cacheName, $messagesUIDs[$a]['_preview']); 
				}
				else
				{
					// Use the cache msgpreview
					$messagesUIDs[$a]['_preview'] = $msgPreview;
				}
			}

			//add error flag if processing appeard to fail
			if( count($messagesUIDs[$a]['_basicHeaders']) == 2 && count($messagesUIDs[$a]) == 7 )
			{
				$messagesUIDs[$a]['error'] = true;
			}
		}

		//update $this->_messages incase used elsewhere, e.g. cached
		if( $this->_messages == null )
		{
			$this->_messages = $messagesUIDs;
		}
		else
		{
			foreach( $messagesUIDs as $message )
			{
				foreach( $this->_messages as $k => $_message )
				{
					if( $message['UID'] == $_message['UID'] )
					{
						$this->_messages[$k] = $message;
						break;
					}
				}
			}
		}
		return $messagesUIDs;
	}
	
	/**
	* recursively traverse parts tree to find first desired text part (html then text part)
	*/
	private function getFirstHumanReadablePartPosStringFromStructureNode(&$bodystructureNode, &$nodeString = '1', &$encoding, &$charset, &$_bestMatchLevel = 0, $_firstIteration = true)
	{
		if( !is_array($bodystructureNode) )
		{
			return false;
		}
	
		if( is_array($bodystructureNode[0]) )
		{

			$bestSubNodeMatchLevel = 0;
			foreach( $bodystructureNode as $k => $v )
			{
				$currentSubNodeString = ($_firstIteration?'':$nodeString . '.') . (string)($k+1);
				$currentBestSubNodeEncoding = null;
				$currentBestSubNodeCharset = null;
				if( $this->getFirstHumanReadablePartPosStringFromStructureNode($v, $currentSubNodeString, $currentBestSubNodeEncoding, $currentBestSubNodeCharset, $currentBestSubNodeMatchLevel, false) )
				{
	
					$bestSubNodeString = $currentSubNodeString;
					$bestSubNodeMatchLevel = $currentBestSubNodeMatchLevel;
					$bestSubNodeEncoding = $currentBestSubNodeEncoding;
					$bestSubNodeCharset = $currentBestSubNodeCharset;
					if( $bestSubNodeMatchLevel == 2 )
					{
						break;
					}	
				}	
			}
			if( $bestSubNodeMatchLevel > 0 )
			{
				$_bestMatchLevel = $bestSubNodeMatchLevel;
				$nodeString = $bestSubNodeString;
				$encoding = $bestSubNodeEncoding;
				$charset = $bestSubNodeCharset;
				return true;
			}
			else
			{
				return false;
			}
		}
	
		$_bestMatchLevel = 0;
		if( strtolower($bodystructureNode[0]) == 'text' && strtolower($bodystructureNode[1]) == 'plain' )
		{
			$_bestMatchLevel = 1;
		}
		elseif( strtolower($bodystructureNode[0]) == 'text' && strtolower($bodystructureNode[1]) == 'html' )
		{
			$_bestMatchLevel = 2;
		}
	
		if( $_bestMatchLevel > 0 ) 
		{
			if( isset($bodystructureNode[5]) )
			{
				$encoding = $bodystructureNode[5];
			}
			if( is_array($bodystructureNode[2]) && strtolower($bodystructureNode[2][0]) == 'charset' )
			{
				$charset = $bodystructureNode[2][1];
			}
			return true;
		}
		else
		{
			return false;
		}
	}
	
	private function getNumberOfAttachmentsFromStructure(&$structure)
	{
		//CONSIDER: consider merging this with some sort of attachment CID filename decoder, and asyncronous attachment structure pos, which also has to be done
		$totalCount = 0;
	
		if( !is_array($structure) )
		{
			return 0;
		}
		else
		{
			for( $a=0 , $structureC = count($structure) ; $a < $structureC ; $a++ )
			{
				//if it has a [6] then part has a size
				if( isset($structure[$a][0]) && is_array($structure[$a][0]) )
				{
					$totalCount += $this->getNumberOfAttachmentsFromStructure( $structure[$a] );
				}
				elseif( count($structure[$a]) > 8
				&& 	isset($structure[$a][6]) 	// ensure we have a byte count after boundry
				&& 	$structure[$a][6] > 1 		// make sure there is something to read
				&& 	(				// test the various structure formats
				($structure[$a][0] != 'text' && $structure[$a][1] != 'plain' && $structure[$a][1] != 'html' )
				||
				(isset($structure[$a][9]) && $structure[$a][9][0] == 'attachment' )
				))
				{
					$totalCount++;
				}
	
			}
			return $totalCount;
		}
		return 0;
	}
	
	private function &_decodeContent($content, $encoding, $charset)
	{
		$encoding = strtolower($encoding);
		$charset = $charset==null?null:strtolower($charset);
	
		if( $encoding == 'quoted-printable' )
		{
			$content = quoted_printable_decode( $content );
		}
		elseif( $encoding == 'base64' )
		{
			$content = base64_decode( $content );
		}
		elseif( $encoding == '7bit' && $charset == 'unicode-1-1-utf-7' )
		{
			$content = iconv( 'ascii', 'UTF-7', $content );
		}

		// only convert character sets to UTF-8 if Content-Type contains charset
		// charset encoded data is human readable so can be a bit more lenient on attempting to decode possible corrupted/badly encoded data
		//convert from charset to UTF-8
		if( $charset == 'iso-8859-1' || $charset == 'iso8859-1')
		{
			$translatedContent = @utf8_encode( $content );
		}
		elseif( $charset == 'unicode-1-1-utf-7' )
		{
			$translatedContent = @iconv( 'UTF-7',  "utf-8//TRANSLIT", $content); 
		}
		elseif( $charset == 'utf4' || $charset == 'utf-4' )
		{
			$translatedContent = @iconv( $charset,  "utf-8//TRANSLIT", $content);
			if($translatedContent == '')
			{
				$translatedContent = @iconv( 'ASCII',  "utf-8//TRANSLIT", $content);
			}
		}
		else
		{
			$translatedContent = @iconv( $charset,  "utf-8//TRANSLIT", $content);
		}
		
		if($translatedContent == '')
		{
			// weird mime type
			// attempt a few decodes
			mb_detect_order("UTF-8,ASCII,ISO-8859-1,windows-1252,windows-1250,iso-8859-15");
			$charset  = mb_detect_encoding($content);
			$translatedContent = @iconv( $charset, "utf-8//TRANSLIT//IGNORE", $content);
		
			if($translatedContent == '')
			{
				$translatedContent = @iconv( $charset, "utf-8//IGNORE", $content);
			}
		}

		return $translatedContent;
	}
	
	public function getMessage($uniqueId)
	{
		
		//TODO : double fetching info, cache was disabled till better implimented
        $this->connect();
		$this->_protocol->store(array("\Seen"), $uniqueId, '', '+');
		
		return new $this->_messageClass(array('handler' => $this, 'id' => $uniqueId) );
		//, 'headers' => $data['RFC822.HEADER'], 'flags' => $flags, 'bodystructure' => $data['BODYSTRUCTURE']));
		
		$cacheName = Atmail_Cache::generateCacheIdString(array($this->getCurrentFolder(),$uniqueId));
		$mail = Atmail_Cache::fetch(Atmail_Cache::TYPE_MAIL_MESSSAGE, $cacheName );
		
		if(empty($mail)) 
		{
		
			$mail = new $this->_messageClass(array('handler' => $this, 'id' => $uniqueId) );
			Atmail_Cache::store(Atmail_Cache::TYPE_MAIL_MESSSAGE, $cacheName, $mail );
		
		}
		return $mail;
		
	}
	
	public function &getThread($uniqueId = null)
	{
		//fetch threads, then find last thread and fetch entire thread for that UID.
		//for convenience $pointerToMessageObject set to point to main message for additional external processing
		if( $uniqueId == null || $uniqueId == '' )
		{
			throw new Atmail_Exception('Error: Need message ID in order to find thread.');
		}
		
		$this->getThreads();
		$threadsC = count($this->_threads);
		for( $a = 0 ; $a < $threadsC ; $a++ ) 
		{
			$returned = &$this->_recursivelyFindRootThread($this->_threads[$a], $uniqueId);
			if($returned !== false)
			{
				break;
			}
		}
		if($returned === false )
		{
			throw new Atmail_Exception('Error: Message ' . $uniqueId . ' not found in threads. Does it still exist?');
		}
	
		return $returned;
	}
	
	private function &_recursivelyFindRootThread(&$threadsNode, $UID)
	{
		if( isset($threadsNode['UID']) && $threadsNode['UID'] == $UID )
		{
			return $threadsNode;
		}
		if( isset($threadsNode['_children']) )
		{
			$childrenC = count( $threadsNode['_children'] );
			for( $a = 0 ; $a < $childrenC ; $a++ )
			{
				$returned = $this->_recursivelyFindRootThread( $threadsNode['_children'][$a], $UID );
				if($returned !== false )
				{
					return $threadsNode;
				}
			}
		}
		$return = false;
		return $return;
	}
	
	public function appendMessage($emailObject, $folder = null, $flags = null)
	{
        $acct = $_REQUEST['acct'];
        if ($acct == null) $acct = '0';

        if ($acct > 0)
        {
            $inbox = new inbox();
            $result = $inbox->append($acct, $emailObject);
            return $result;
        }

        $this->connect();
		
		// Replace \n with \r\n for better compatibility with all IMAP servers when issuing APPEND
		$emailObject = preg_replace("/(?<!\r)\n/", "\r\n", $emailObject);
		
		// Zend_Mail_Storage_Imap does not return the response from
		// the append(), we need this to get the message UID so we'll
		// implement the append here and extract the uid from the response
		//parent::appendMessage($emailObject, $folder, $flags);
		
		if ($folder === null) {
            $folder = $this->_currentFolder;
        }

        if ($flags === null) {
            $flags = array(Zend_Mail_Storage::FLAG_SEEN);
        }

        // TODO: handle class instances for $message
        if (!$res = $this->_protocol->append($folder, $emailObject, $flags)) {
            /**
             * @see Zend_Mail_Storage_Exception
             */
            require_once 'Zend/Mail/Storage/Exception.php';
            throw new Zend_Mail_Storage_Exception('cannot create message, please check if the folder exists and your flags');
        }
        
        // extract the uid from the response
        foreach ($res as $r) {
			if (preg_match("/\[APPENDUID\s\d+\s(\d+)\]/", $r, $m)) {
				return $m[1];
			}
		}
		return false;
	}
	
	// FOLDER FUNCTION
	////////////////////////////////////////////////////////////////////////////////
	
	/**
	* get root folder or given folder
	*
	* @param  string $folderRootNameGlobal get folder structure for given folder, else root
	* @return Zend_Mail_Storage_Folder root or wanted folder (UTF&-IMAP decoded to UTF-8)
	* @throws Atmail_Mail_Exception
	*/
	public function getFreshFolders( $folderRootNameGlobal = null )
	{
		$this->connect();
		$folders = $this->_protocol->listMailbox( (string)$folderRootNameGlobal );
		if( !$folders )
			throw new Atmail_Mail_Exception('folder not found');
	
		// Folders are UTF7-IMAP
		ksort($folders, SORT_STRING); //TODO: confirm that sorting on utf7 does generate valid results
		$root = new Atmail_Mail_Storage_Folder('/', '/', false);
		$stack = array(null);
		$folderStack = array(null);
		$parentFolder = $root;
		$parent = '';
	
		foreach( $folders as $globalName => $data )
		{
			do 
			{
				if( !$parent || strpos($globalName, $parent) === 0 )
				{
					$lastDelimPos = strrpos($globalName, $data['delim']);
					if( $lastDelimPos === false )
						$localName = $globalName;
					else
					{ 
						$localName = substr($globalName, $lastDelimPos + 1);
						
						// Does the parent exist?
						// If not we want to display the folder name as "parent.child"
						// rather than just "child"
						$parentGlobalName = substr($globalName, 0, $lastDelimPos);
						
						if (!array_key_exists($parentGlobalName, $folders))
						{
							$lastDelimPos = strrpos($parentGlobalName, $data['delim']);
							$parentLocalName = substr($parentGlobalName, $lastDelimPos + 1);
							$localName = $parentLocalName . $data['delim'] . $localName;
						}
					}
					
					$selectable = true;
					if( array_key_exists('flags', $data) )
					{
						
						foreach( $data['flags'] as $flag )
						{
						
							if( strpos( strtolower($flag), 'noselect') !== false )
								$selectable = false;
							
						}   
						
					}
										
					array_push($stack, $parent);
					$parent = $globalName . $data['delim'];
					$folder = new Atmail_Mail_Storage_Folder($localName, $globalName, $selectable);
					$parentFolder->$localName = $folder;
					array_push($folderStack, $parentFolder);
					$parentFolder = $folder;
					break;
			
				}
				else if( $stack )
				{
			
					$parent = array_pop($stack);
					$parentFolder = array_pop($folderStack);
			
				}
			
			}
			while( $stack );
			
			if( !$stack )
				throw new Atmail_Mail_Exception('error while constructing folder tree');

		}
		
		$this->_folders =  $root;
		$this->_session->_folders = $this->_folders;
		
		return $this->_folders;
	}
	
	public function getFolders( $rootFolder = null )
	{
		
		if( $this->_folders != null )
		{
			
			return $this->_folders;
			
		}
		else
		{
			
			return $this->getFreshFolders( $rootFolder );
			
		}	
		
	}
	
	public function getCurrentFolder()
	{
		
		if( isset($this->_session->folderNameCurrentGlobal) )
		{
			return $this->_session->folderNameCurrentGlobal;
		}
		
		$this->connect();
		return parent::getCurrentFolder();
		
	}
	
	public function getCurrentFolderObject()
	{
		
		return $this->getFolderObject( $this->getCurrentFolder() );
	
	}
	
	/**
	* select $folder
	* N.B. must be selectable!
	*
	* @param  Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder
	* @return null
	* @throws Zend_Mail_Storage_Exception
	* @throws Zend_Mail_Protocol_Exception
	*/
	public function selectFolder( $folderNameGlobal )
	{
		
		$this->connect();
	
		if ($folderNameGlobal instanceof Atmail_Mail_Storage_Folder) {
			$folderNameGlobal = $folderNameGlobal->getGlobalName();
		}
		
		$this->_session->folderNameCurrentGlobal = $folderNameGlobal; //TODO: remove duplication with parent variable
		$this->_currentFolder = $folderNameGlobal; //parent field set incase any nonoverloaded functions depend on this
	
		$result = $this->_protocol->select( $folderNameGlobal );
	
		if( !$result )
		{
			$this->_currentFolder = '';
			throw new Atmail_Exception('cannot change folder. Does it still exist?');
		}
	
		//if changing folder then reset datastores
		$this->_messages = null;
		$this->_threads = null;
	
		$this->_foldersInfo[$folderNameGlobal] = $result;
		return $result;
		
	}
	
	public function getFreshFolderInfo($folderNameGlobal = null )
	{
		
		$this->connect();
		if( $folderNameGlobal == null )
		{
			
			$folderNameGlobal = $this->getCurrentFolder();
			
		}
		
		$folderInfo = $this->_protocol->examine( $folderNameGlobal );
		if( $folderInfo )
		{
			
			//initialize _foldersInfo if not already done
			if( !is_array($this->_foldersInfo) )
			{
				$this->_foldersInfo = array();
			}
			
			if( $folderInfo['exists'] > $this->getThreadingMaxLimit() )
			{

				$this->setThreadsEnabled( false );

			}
			$this->_foldersInfo[$folderNameGlobal] = $folderInfo;
			return $this->_foldersInfo[$folderNameGlobal];
			
		}
		else
		{
			
			throw new Atmail_Exception('Unable to get folder info. Does it still exist?');
			
		}	
		
	}
	
	public function getFolderInfo( $folderNameGlobal = null )
	{
		
		if( $folderNameGlobal == null )
		{
			
			$folderNameGlobal = $this->getCurrentFolder();
			
		}
		
		if( !isset( $this->_foldersInfo[$folderNameGlobal] ) )
		{
			
			$this->getFreshFolderInfo( $folderNameGlobal );
		 
		}
		
		//set threads enabled for the current folder
		//TODO: when revising cache change to having a flag for each folder 
		
		if( $this->_foldersInfo[$folderNameGlobal]['exists'] > $this->getThreadingMaxLimit() )
		{

			$this->setThreadsEnabled( false );

		}
		return $this->_foldersInfo[$folderNameGlobal];
	
	}
	
	public function getFolderObject( $folderNameGlobal = null )
	{
		//search for a global Name in folders and return object reference if found
		$folders = new RecursiveIteratorIterator($this->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
		foreach( $folders as $folder => $object )
		{
			if( $folderNameGlobal == $object->getGlobalName() )
				return $object;
			
		}
		
		//get list fo folders and look for common names for the inbox (can also add more rules here for matching inbox)
		foreach( $folders as $folder => $object )
		{
			if( strtolower($folder) == 'inbox') //in_array($folder, array('INBOX', 'Inbox', 'inbox')) )
				return $object;
			
		}
		return null;
	}	
	
	/**
	 *
	 * @param String $folderNameGlobal: Must NOT be null
	 * @return folderObject|null 
	 */
	public function getFolderObjectByName( $folderNameGlobal )
	{
		if($folderNameGlobal == null) return null;	
		
		//search for a global Name in folders and return object reference if found
		$folders = new RecursiveIteratorIterator($this->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
		foreach( $folders as $folder => $object )
		{
			if( $folderNameGlobal == $object->getGlobalName() )
				return $object;
			
		}
		
		return null;
	}
	
	/**
	* create a new folder
	*
	* @param  string						  $name		 global name of folder, local name if $parentFolder is set
	* @param  string|Zend_Mail_Storage_Folder $parentFolder parent folder for new folder, else root folder is parent
	* @return folderNameGlobalNew
	* @throws Atmail_Mail_Exception
	*/
	public function createFolder($folderNameNew, $parentFolder = null, $global = false)
	{
		
		$this->connect();
		
		if( $parentFolder instanceof Zend_Mail_Storage_Folder ||  $parentFolder instanceof Atmail_Mail_Storage_Folder )
		{
		
			$folderNameGlobalNew = $parentFolder->getGlobalName() . $this->getFolderDelimiterDefault() . $folderNameNew;
		
		}
		elseif( $parentFolder != null )
		{
		
			$folderNameGlobalNew = $parentFolder . $this->getFolderDelimiterDefault() . $folderNameNew;
		
		}
		else
		{
		
			$namespaceDefault = $this->getFolderNamespaceDefault();
			$delimiterDefault = $this->getFolderDelimiterDefault();
			if( $global || strlen($namespaceDefault) == 0 || $delimiterDefault == 'NIL' )
			{
				//try create folder in root
				$folderNameGlobalNew = $folderNameNew;

			}
			else
			{

				//create folder in default namespace with default delim
				$folderNameGlobalNew = $namespaceDefault . $delimiterDefault . $folderNameNew;

			}
		   
		}
	
		if ( !$this->_protocol->create( $folderNameGlobalNew ) )
		{
			
			throw new Atmail_Mail_Exception('cannot create folder. Possibly already exists');
			
		}
		
		//expire _folders, until _folder cleanup implimented;
		$this->_session->_folders = null;
		$this->_folders = null;

		return $folderNameGlobalNew;
	
	}
	
	/**
	* rename and/or move folder
	*
	* The new name has the same restrictions as in createFolder()
	*
	* @param  string|Zend_Mail_Storage_Folder $folderOld name or instance of folder
	* @param  string						  $folderNameGlobalNew new global name of folder
	* @return folderNameGlobalNew
	* @throws Atmail_Mail_Exception
	*/
	public function renameFolder($folderOld, $folderNameGlobalNew)
	{
		$this->connect();
		if( $folderOld instanceof Zend_Mail_Storage_Folder )
		{
			$folderOld = $folderOld->getGlobalName();
		}

		//N.B. IMAP server wont allow you to change the name of a currently selected folder
		if( $folderOld == $this->_config['folder'] || $folderOld == $this->_session->folderNameCurrentGlobal || $folderOld == $this->_currentFolder )
		{
			$this->selectFolder( $this->getMainFolderName() );
			if (!$this->_protocol->rename( $folderOld, $folderNameGlobalNew ) )
			{
				$this->selectFolder( $folderOld );
				throw new Atmail_Mail_Exception('cannot rename folder');
			}
			
			$this->selectFolder($folderNameGlobalNew);
			$this->_config['folder'] = $folderNameGlobalNew;
			$this->_session->folderNameCurrentGlobal = $folderNameGlobalNew;
		}
		else
		{
	
			if( !$this->_protocol->rename( $folderOld, $folderNameGlobalNew ) )
			{
				throw new Atmail_Mail_Exception('cannot rename folder');
			}
		}
	
		//expire _folders, until _folder cleanup implimented;
		$this->_session->_folders = null;
		$this->_folders = null;
	
		return $folderNameGlobalNew;
	}
	
	/**
	* remove a folder
	*
	* @param  string|Zend_Mail_Storage_Folder $folder	  name or instance of folder
	* @return null
	* @throws Atmail_Mail_Exception
	*/
	public function removeFolder($folder)
	{
		$this->connect();
		if( $folder instanceof Zend_Mail_Storage_Folder )
		{
			
			$folder = $folder->getGlobalName();
			
		}
		
		//$mainFolderNameInbox = $this->getMainFolderName();
		//$currentFolder = $this->getCurrentFolder();
		//CONSIDER switch out of current folder if a subfolder of what is being deleted.
		//if( $folder == $this->_config['folder'] || $folder == $this->_session->folderNameCurrentGlobal || $folder == $this->_currentFolder ) 
		//{
		//	$this->_config['folder'] = $mainFolderNameInbox;
		//	$this->_session->folderNameCurrentGlobal = $mainFolderNameInbox;
		//	$this->selectFolder( $mainFolderNameInbox );
		//}
		
		if( !$this->_protocol->delete( $folder ) )
		{
			//TODO: translate
			throw new Atmail_Mail_Exception('cannot delete folder');
		
		}
	
		//expire _folders, until _folder cleanup implimented;
		$this->_session->_folders = null;
		$this->_folders = null;
		
	}
	
	public function getFolderNamespaceDefault()
	{
		$this->connect();
		return $this->_protocol->getFolderNamespaceDefault();
	}
	
	public function getFolderDelimiterDefault()
	{
		$this->connect();
		return $this->_protocol->getFolderDelimiterDefault();
	}
	
   
	/**
	* Returns global name of a main folder as required by different IMAP servers
	*/
	public function getMainFolderName( $localMainFolderName = '' )
	{
		
		//mainFolderName implies Main folder as in reserved folder needed for normal function of the mail client
		//Main Folders= INBOX, Drafts, Trash, Spam
		//TODO: search custom folder mappinglist first and return match else calculate next
		//TODO: then search knows server brand default folder mappings
		//TODO: at this point still dont know which folder new email gets delivered into, INBOX assumed
		
		//default is to have reserved folders inside the default namespace using the defauly folder delimiter
		
		//handle request for root folder
		if( strlen( $localMainFolderName ) == 0 || strtoupper($localMainFolderName) == 'INBOX' )
		{
		
			//get list fo folders and look for common names for the inbox (can also add more rules here for matching inbox)
			$folders = $this->getFolders();
			foreach( $folders as $folderNameGlobal => $folderData )
			{
				if( in_array($folderNameGlobal, array('INBOX', 'Inbox', 'inbox')) )
					return $folderNameGlobal;
				
			}
			return 'INBOX';
		
		}
		
		$mainFolderName = ( strlen($this->getFolderNamespaceDefault()) ==0?$localMainFolderName:$this->getFolderNamespaceDefault() . $this->getFolderDelimiterDefault() . $localMainFolderName);
		
		return $mainFolderName;
		
	}

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

	public function setSortAndOrder($sort = null, $order = null)
	{
		
		//N.B. only sets the sort and order fields, doesnt actually do the sorting. (late = only sorted when data called)
		//consider rather storing sort and order in the scope _session var so that sort doesn't need to be called before dependand data calls/use
		//handle folders with <2 messages
	
		//translate necessary plain english sort and order into protocol format
		
		$sort = strtoupper($sort);
		//commented out to stop force sorting (heavy) when not requested.
		//if( $sort == 'DATE' ||  $sort == '' ||  $sort == null )
		//	$sort = 'DATE';
		if( $order == null || in_array(strtoupper($order), array('DESC', 'REVERSE')) )
			$order = 'REVERSE';
		else
			$order = ''; //opposite of IMAP REVERSE is ''
			
		if( $this->_sort != $sort )
		{
			$this->_sort = $sort;
			//sort changed so if we already have _threads then cached so need to resort so _threads matched sort order
			$this->_messages = null;
			
			//only sort threads if enabled/set
			if( $this->_threads )
				$this->_sortUnsortedThreads();
						
			//use native IMAP commands like: SORT (ARRIVAL) us-ascii ALL
			//just change and zero _messages (LATE) other methods wil only fetch what they need so dont fetch all here
			//$this->_sort = $sort;
			//TODO: more inteligent internal sorting of able fields before opting to nuke viable data
			//$this->_messages = null;
			//$this->_threads = null;
			//$this->_threadsLinear = null;	
		} 
		
		if( $this->_order != $order )
		{
			//simply reverse the arrays
			if( is_array($this->_messages) )
			{
				$this->_messages = array_reverse($this->_messages);
			}
			if( is_array($this->_threads) )
			{
				$this->_threads = array_reverse($this->_threads);
			}
			$this->_order = $order;
		}
		//CONSIDER: sorting without new call to imap server if we already have headers needed for required field sorting
		//CONSIDER: multiple field sorting
	}
	
	
	// CACHE FUNCTIONS
	////////////////////////////////////////////////////////////////////////////////
	public static function createCache($config = null)
	{
		
		if( !isset($config) )
		{
			return array();
		}
		
		$namespaceName = isset($config['namespaceName'])?$config['namespaceName']:self::$_namespaceName;
		$session = new Zend_Session_Namespace($namespaceName);
		if( !isset($config['folder']) )
		{
			if( isset($session->folder) )
			{
				$config['folder'] = $session->folder;
			}
			elseif( isset($config['defaultFolder']) )
			{
				$config['folder'] = $config['defaultFolder']; //$config not persistent but useful further on
			}
			else
			{
				$config['folder'] = 'INBOX';
			}
		}
		if( !isset($config['tmpFolderBaseName']) )
		{
			throw new Atmail_Mail_Exception('Compulsory tmpFolderBaseName not found in args for IMAP class');
		}
		
		return $config;
	}
	
	public static function tryLoadFromCache($config)
	{
		$config = self::createCache($config);	
		$folder = isset($config['folder'])?$config['folder']:$config['defaultFolder'];
		
		//folder wont be set on first hit and no cache data would be valid so return false
		if( !isset($folder) ) 
		{
			return false; 
		}
		
		if( ($cached = Atmail_Cache::fetch(Atmail_Cache::TYPE_MAIL_STORE, Atmail_Cache::generateCacheIdString(simplifyString($folder)))) !== false)
		{
			$cached->_connected = false;
		}
		return $cached;
	}
	
	/**
	* Pseudo send recieve
	* does light stream hit and culls or adds messages to the view. 
	*/
	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/should make more generic if usage expands. (e.g. fetched flag for each message)
		//Make copy of old then reconnect to storage and compare
		//Class destructor closes remote link so need to do some simple swapping of old and new to avoid writing a whole lot of extra code to handle
		//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->_connected)
		//	$this->connect();
		//reset _folders folder objects
		$this->_folders = null;
		$this->_foldersInfo = null;
		
		//because there is no reliable way of detecting if folder status has changed we have to nuke it completely.
		//pending a bright idea on how to detect a deleted message, added message, and also pending a bright idea on appending a net thread message onto existing threads without a complete refetch (light hit)
		
		$this->_threads = null;
		$this->_messages = null;
		
		//now possilby consider updating message list and threads list if populated and _foldersInfo changed for current folder.
		//TODO: inteligently scan for threads newer than latest cached id and merge in, but for now just refetch entire
		$this->updateCacheStamp = time();
	}   
	
	public function saveCache()
	{
		$currentFolder = '';
		if( isset($this->_session->folderNameCurrentGlobal) )
		{
			$currentFolder = $this->_session->folderNameCurrentGlobal;
			$this->dataSource = 'cached';
			return Atmail_Cache::store(Atmail_Cache::TYPE_MAIL_STORE, Atmail_Cache::generateCacheIdString(simplifyString($currentFolder)), $this );
		}
		// TODO: This is currently disabled.
		//caching folder store problematic and folders getting confused so disabling cache saving until reworked (will make a massive difference)	
		//return true;
		return false;

	}

	static function decodeFilterData($filtername, $rawdata)
	{
		// 1. base64 decode
		// 2. gunzip
		// 3. convert to array
	}

	static function getFilterFromDatabase($filtername)
	{
		$filterKeywords = array();

		if($filtername == 'friends') {

			$account = users::getAccount();
			$filterKeywords = users::getFriendsEmails( $account );
			
		}
		else if($filtername == 'purchases') {
			$filterKeywords = array('purchase','invoice','payment','receipt');
		}
		else if($filtername == 'notifications') {
			$filterKeywords = array(
				'ebay.com',
				'linkedin.com',
				'twitter.com',
				'facebookmail.com',
			);
		}
		else if($filtername == 'newsletters') {
			$filterKeywords = array('"list-unsubscribe" ""',);
		}
		
		return $filterKeywords;
	}

	/**
	 *
	 * @filtername:  purchases, notifications
	 */
	static function getFilterFromDNS($filtername)
	{

		//!!! this function cannot be used before DNS data available

		//TODO: use DNS for filter words

		$filterDomains = array(
			'purchases' => 'purchases.filter._api.atmail.com',
			'notifications' => 'notifications.filter._api.atmail.com',
		);

		$domain = $filterDomains[$filtername];

		$filterKeywords = array();

		if($domain) {
			$rawdata = dns_get_record($domain, DNS_TXT);
			$filterKeywords = self::deocdeFilterData($filtername, $rawdata);
		}

		return $filterKeywords;
	}
	
	/**
	 * @return array of keyword
	 */
	static function getFilterKeywords($filtername)
	{
		// define rules for loading from db or dns
		$loadFromDb = true; // TODO: make rules

		// friends must load from db
		if($filtername == 'friends') {
			$loadFromDb = true;
		}
		
		return $loadFromDb? self::getFilterFromDatabase($filtername) : self::getFilterFromDNS($filtername);
	}

	static function splitArrayIntoSmall($countLimit, $filterKeywords)
	{
        $i = 0;
        $filterKeywordsArray = array();

        foreach($filterKeywords as $keyword) {
            if($i==0) {
              $items = array($keyword,);
            }
            else
              $items[] = $keyword;

            ++$i;

            if($i==$countLimit) {
                $filterKeywordsArray[] = $items;
                $i=0;
                $items = array();
            }

        }

        if(count($items)>0) $filterKeywordsArray[] = $items;

        return $filterKeywordsArray;
	}

	static function buildMultiOrCommand($filterKeywords, $filtername)
	{
		$countLimit = 10;

		$searchPoint = 'FROM';
		$searchPointMap = array(
			'purchases' => 'SUBJECT',
			'newsletters' => 'HEADER',
		);

		if(isset($searchPointMap[$filtername])) {
			$searchPoint = $searchPointMap[$filtername];
		}

		$filterKeywordsArray = self::splitArrayIntoSmall($countLimit, $filterKeywords);
		$commands = array();

		foreach($filterKeywordsArray as $keywords) {
			$count = count($keywords);

			$field = '';
			for($i=0; $i<$count-1; ++$i) {
				$field .= ' OR';
			}

			$valueString = '';
			foreach($keywords as $keyword) {
				$valueString .= $searchPoint . ' ' . $keyword . ' ';
			}

			$commands[] = '(' . $field . ' ' . $valueString . ')';
		}

		return $commands;
	}



	// 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($searchArray, $sortResults = true)
	{
		// search capability 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
		$this->connect();
		/*
		SUDOCODE:
		for each $args as search term
		Create IMAP search string
		requestAndRepsonse 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',
		'value' => 'TEXT',
		'body' => 'TEXT',
		'text' => 'TEXT',
		'unseen' => 'UNSEEN'
		);
	
		$searchArgs = array();

		$multipleFilter = null;
			
		//add a since limit by default
		//$searchArray[] = array('field' => 'date', 'modifier' => 'after', 'value' => '1-Jan-2008');
		$dateLimitSet = false;
		foreach( $searchArray as $searchTerm )
		{
			if( $searchTerm['field'] == 'raw' )
				$searchArgs[] = '(' . $searchTerm['value'] . ')';
			else
			{
				$valueString = $searchTerm['value'];
				
				//check for range terms and handle seperately
				if( $searchTerm['field'] == 'size' )
				{
					if( $searchTerm['modifier'] == 'lessthan' )
						$field = 'SMALLER';
					else
						$field = 'LARGER';
				}
				elseif( $searchTerm['field'] == 'date' )
				{
					$dateLimitSet = true;
					if( $searchTerm['modifier'] == 'lessthan' )
						$field = 'BEFORE';
					else
						$field = 'SINCE';
					$valueString = date('j-M-Y', strtotime($valueString));//TODO: use browsers configured time, not server time (PHP current/default)
				}
				elseif( $searchTerm['field'] == 'since' )
				{
					$dateLimitSet = true;
					$field = 'SINCE';
					$valueString = date('j-M-Y', strtotime($valueString));//TODO: use browsers configured time, not server time (PHP current/default)
				}
				elseif( $searchTerm['field'] == 'before' )
				{
					$dateLimitSet = true;
					$field = 'BEFORE';
					$valueString = date('j-M-Y', strtotime($valueString));//TODO: use browsers configured time, not server time (PHP current/default)
				}
				elseif( array_key_exists($searchTerm['field'], $ImapField))
				{
					$field = $ImapField[$searchTerm['field']];
					$valueString = '"' . $valueString . '"';
				}
				elseif( $searchTerm['field'] == 'filter' )
				{
					$multipleFilter = $searchTerm['value'];
				}
				else
				{
					$field = $searchTerm['field'];
					$valueString = '"' . $valueString . '"';
				}
				
				if( $searchTerm['field'] != 'filter' )
					$searchArgs[] = '(' . $field . ' ' . $valueString . ')';

					//$searchString .= ' ' . $field . ' ' . $valueString . '';  
			}
		}
		$searchString = 'UID SEARCH ';
		if( !$dateLimitSet && strpos(strtolower(implode($searchArgs, ' ')), 'text ') !== false )
			$searchString .= 'SINCE ' . trim(strftime("%e-%b-%G", strtotime("-1 year"))) . ' ';
		
		$searchString .= implode($searchArgs, ' ');

		if($multipleFilter != null) {

			$filterKeywords = self::getFilterKeywords($multipleFilter);

			//file_put_contents("/tmp/filter.log", "\n================\nfilterKeywords= " . print_r($filterKeywords,1), FILE_APPEND);

			$commands = self::buildMultiOrCommand($filterKeywords, $multipleFilter);

			$searchCommand = $searchString;

			$searchResultsTmp = array();

			foreach($commands as $command) {
				$searchString = $searchCommand . ' ' . $command;

				$result = $this->_protocol->requestAndResponse($searchString);
				//file_put_contents("/tmp/filter.log", "\nIMAP Command=> $searchString \n", FILE_APPEND);
				if(empty($result)) {
					//file_put_contents("/tmp/filter.log", "\nERROR: IMAP Command failed=> $searchString \n", FILE_APPEND);
					continue;
				}

				$searchResultsTmp = array_merge($searchResultsTmp, $result);
				//file_put_contents("/tmp/james.log", "\nmergeresult=". print_r($searchResultsTmp,1), FILE_APPEND);
			}

			$searchResults = array(
				0 => array( "SEARCH",),
			);

			foreach ($searchResultsTmp as $ids) 
			{
				if( $ids[0] == 'SEARCH' && count($ids) > 1 ) {
					//pop the SEARCH item off the array
					array_shift($ids); 			
					
					$searchResults[0] = array_merge( $searchResults[0], $ids );		
				}
			}

			$searchResults[0] = array_unique($searchResults[0]);
			//file_put_contents("/tmp/james.log", "\n FINAL result=". print_r($searchResults,1), FILE_APPEND);


		}
		else {
			//echo $searchString;
			$searchResults = $this->_protocol->requestAndResponse($searchString);
		}

		$this->_messages = null;
		if ( !is_array($searchResults) || !is_array($searchResults[0]) || $searchResults[0][0] != 'SEARCH' || count($searchResults[0]) < 2 ) 
		{
		
			$this->_messages = array();
			return $this->_messages;
		
		}
		
		$resultsSorted = array();
		//some servers issue complaints about the search query before giving OK and the result to work through them ignoring them until you find the result.	


		// parse search result and store it into an array
		$searchResultUIDs = array();
		foreach ($searchResults as $ids) 
		{
			if( $ids[0] == 'SEARCH' && count($ids) > 1 ) 
			{

				//pop the SEARCH item off the array
				array_shift($ids); 

				$searchResultUIDs = array_merge($searchResultUIDs, $ids);
			}
		}

		$ids = $searchResultUIDs;

		if(count($ids))
		{

			// FIXME: sort will take very long time for large amount. need better way to do that.
			if(count($ids) > 500) {
				$sortResults = FALSE;
			}

			//TODO: remove sort from this method - prevents reuse
			if( $sortResults ) 
			{

				$resultsSorted = array();
				//get sorted list ready to intersect with search results.
				$this->_messages = $this->getUniqueIds();

				if(count($this->_messages) < 500 ) 
				{

					//now sort on the sort order my finding intersection of sorted _messages and matched UIDs.
					foreach ($this->_messages as &$_message) 
					{
						if( in_array($_message['UID'], $ids) ) 
						{
							$resultsSorted[] = array('UID' => $_message['UID']);
						}
					}

					$this->_messages = $resultsSorted;
				}
				else 
				{
					$ids = array_reverse($ids);

					$this->_messages = array();
					foreach( $ids as &$id )
					{
						$this->_messages[] = array('UID' => $id );
					}

				}

				return $this->_messages;
			
			}
			else
			{
				$ids = array_reverse($ids);

				$this->_messages = array();
				foreach( $ids as &$id )
				{
					$this->_messages[] = array('UID' => $id );
				}
				return $this->_messages;
				
			}
				
		}
		return array();		

	}
	
	/**
	* Encode a UTF-8 string to UTF7-IMAP
	*
	* @param string $str The string to encode
	* @access public
	* @return string
	*/
	public function encodeUTF7( $string )
	{
		
		//TODO: remove overide
		if ( true || (function_exists('mb_convert_encoding') && $this->_config['allowUTF7Folders']) )
		{
			return mb_convert_encoding($string, 'UTF7-IMAP', 'UTF-8');
		}
		else
		{
			return $string;
		}
	}
	
	
	/**
	* Decode a UTF7-IMAP string to UTF-8
	*
	* @param string $string The string to decode
	* @access public
	* @return string
	*/
	public function decodeUTF7($string)
	{
		
		//TODO: remove overide
		if ( true || (function_exists('mb_convert_encoding') && $this->_config['allowUTF7Folders']) )
		{
			return mb_convert_encoding($string, 'UTF-8', 'UTF7-IMAP');
		}
		else
		{
			return $string;
		}
	}
	
	/**
	* Return a subject with Re: if required
	*
	* @param string $string The subject to append
	* @access public
	* @return string
	*/
	public function reReply($subject)
	{
		
		if (!preg_match('/^\s*Re.*?:/i', $subject) && !preg_match('/^Ynt:/i', $subject)
		&& !preg_match('/^TR/i', $subject) && !preg_match('/^Oggetto:/i', $subject)
		&& !preg_match('/^VS:/i', $subject) && !preg_match('/^Awt:/i', $subject)
		&& !preg_match('/^Aw:/i', $subject) && !preg_match('/^TR:/i', $subject)
		&& !preg_match('/^R:/i', $subject) && !preg_match('/^RES:/i', $subject))
		$subject = "Re: " . $subject;
		
		return $subject;
	}

/**
* Return a subject with Re: if required
*
* @param string $subject the subject to search and/or append
* @param string $prefix the prefix to search and append
* @access public
* @return string
*/

function setResponsePrefix($subject,$prefix)
{
  if(!isset($prefix)){
    return $this->reReply($subject);
  }

	if( preg_match('/^\s*(re|fwd*)\:/i',$subject) ){
  	return preg_replace('/^\s*(re|fwd*)\:/i',$prefix,$subject);
	} else {
		return "{$prefix} {$subject}";
	}
}

	
	public function setFlagsAdvanced($emailFolder, $UIDs, $flags, $mode)
    {
        if (DEBUG_FULL) $stopwatch = new Stopwatch();
        if (DEBUG_FULL) debugLog(__FILE__, __METHOD__, __LINE__, 'setFlagsAdvanced START');

        $success = true;

        $UidArr = explode(',', $UIDs);

        foreach ($UidArr as $UID)
        {
            $ids = explode(':', $UID);
            if (count($ids) != 2)
            {
                Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": Bad message id: $UID. Was not in the form of acct:msgid.");
                continue;
            }

            if ($ids[0] === '0')
            {
                $success = $success && $this->_protocol->store($flags, $ids[1], null, $mode);
                if (DEBUG_FULL) debugLog(__FILE__, __METHOD__, __LINE__, 'setFlagsAdvanced: local: ' . $stopwatch->clock());
            }
            else
            {
                $emailAcct = $ids[0];
                $emailUid = $ids[1];

                $inbox = new inbox();
                $email = $inbox->updateFlag($emailAcct, $emailFolder, $emailUid, $flags, $mode);
                if (DEBUG_FULL) debugLog(__FILE__, __METHOD__, __LINE__, "setFlagsAdvanced: external: acct: $emailAcct, uid: $emailUid: " . $stopwatch->clock());
            }
        }

        if( !$success )
        {
      	    throw new Zend_Mail_Storage_Exception('cannot set flags, have you tried to set the recent flag or special chars?');
        }

        if (DEBUG_FULL) debugLog(__FILE__, __METHOD__, __LINE__, 'setFlagsAdvanced: Done: ' . $stopwatch->elapsed());

		return $success;
    }

	public function getFlags($UID)
	{
		$flags = array();
		$result = $this->_protocol->fetch(array('UID', 'FLAGS'), $UID);
		//sanity check result
		if( !is_array($result) || !array_key_exists('FLAGS',$result) || !is_array($result['FLAGS']) )
		{
			Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": Bad response received while trying to get message flags for " . $UID . ': ' . print_r($result,true));
			return $flags;
		}
		foreach( $result['FLAGS'] as $v)
			$flags[$v] = $v;
		
		return $flags;
	}

    /**
     * move an existing message
     *
     * NOTE: imap has no native move command, thus it's emulated with copy and delete
     *
     * @param  int                             $id     number of message
     * @param  string|Zend_Mail_Storage_Folder $folder name or instance of target folder
     * @return null
     * @throws Zend_Mail_Storage_Exception
     */
//    public function moveMessage($id, $fromFolder, $folder)
    public function moveMessage($id, $folder)
    {
        $ids = explode(':', $id);

        if (count($ids) != 2)
        {
            Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": Bad message id: $id. Was not in the form of acct:msgid.");
            return;
        }

        $emailUid  = array_pop($ids);
        $emailAcct = array_pop($ids);

        if ($emailAcct === '0')
        {
            $this->copyMessage($emailUid,$folder);
            $this->removeMessage($emailUid);
        }
        else
        {
            $inbox = new inbox();
            $fromFolder = urldecode($_REQUEST['fromfolder']);
            $inbox->moveMessage($emailAcct, $emailUid, $fromFolder, $folder);
        }
    }
}

function cmpUids($a, $b)
{
	if ($a['UID'] == $b['UID'])
	{
		return 0;
	}
	return ($a['UID'] < $b['UID']) ? -1 : 1;
}


// Groups array of message uids by UID, _firstTextPart and omit previously preview generated messages
function group_uids($array, $check_cache)
{
	$ret  = array();
	$temp = array();

	// sort UIDS into decending order! (server compat)
	usort($array, "cmpUids");
	foreach($array as $val)
	{
		if($val['UID']<=0) continue;
		$test = next($array);
		$hasPreview = ((isset($val['_preview']) && $val['_preview'] != '') ? true : false );
		if(!$hasPreview)
		{
			if($test['UID'] == ($val['UID'] + 1) && $test['_firstTextPart'] ==  $val['_firstTextPart'])
			{
				$temp[] = $val['UID'];
			}
			else
			{
				if(count($temp) > 0)
				{
					$ret[] = array('range' => $temp[0].':'.$val['UID'], '_firstTextPart' => $val['_firstTextPart']);
					$temp   = array();
				}
				else
				{
					$ret[] = array('range' => $val['UID'], '_firstTextPart' => $val['_firstTextPart']);
				}
			}
		}
		else
		{
			if(count($temp) > 0)
			{
				$ret[] = array('range' => $temp[0].':'.$val['UID'], '_firstTextPart' => $val['_firstTextPart']);
				$temp   = array();
			}
		}
	}
	return $ret;
}
