<?php
/**
 * Atmail
 * 
 * @category   Atmail
 * @package    Atmail_Mail
 * @subpackage Protocol
 * @copyright  Copyright (c) NetBased Software pty ltd
 * @license    
 */

/**
 * We want to extend the Zend_Mail_Protocol_Imap class to add some
 * extra functionality and override certain functions to provide UID support.
 *
 * @category   Atmail
 * @package    Atmail_Mail
 * @subpackage Protocol
 * @copyright  Copyright (c) NetBased Software pty ltd
 */
require_once 'Zend/Mail/Protocol/Imap.php';

class Atmail_Mail_Protocol_Imap extends Zend_Mail_Protocol_Imap {
    
	
	public $_previousCommand;
	
	private $log;
	private $_capability = null;
	private $_namespaces = null;
	private $_IMAPQueryMaxLength = 65536;
	
	public function __construct($params = null) 
	{ 
		
		$this->log = Zend_Registry::get('log');
				
	} 
	
    /**
	 * looking at overloading to enforce UID for all commends except ones that cant have UID in the command 
	 * there looks like there is a simpler way to achieve this so this will prob change
	 * revise all IMAP commands here to attempt to prevent having to overload many methods
	 * currently catching:
	 *        folder names and converting to utf7 if enabled
	 *        FETCH commands and converting to UID commands
	 * send a request
     *
     * @param  string $command your request command
     * @param  array  $tokens  additional parameters to command, use escapeString() to prepare
     * @param  string $tag     provide a tag otherwise an autogenerated is returned
     * @return null
     * @throws Atmail_Mail_Protocol_Exception
     */
    public function sendRequest($command, $tokens = array(), &$tag = null, $forceOrdinalIDs = false ) 
	{
    
    	if (!$tag) 
		{
        
    		++$this->_tagCount;
            $tag = 'TAG' . $this->_tagCount;

        }
        
		if( in_array( strtoupper( $command ), array('FETCH', 'COPY', 'SEARCH', 'SORT') ) && strtoupper( substr($command, 0, 3) ) != 'UID' && !$forceOrdinalIDs )
        	$appendToCommand = 'UID ';
		else
		    $appendToCommand = '';
		
		//CONSIDER: add disabled flag if customers demand instances where third party IMAP clients may get confused be UTF7 encoded folder names (may prefer other/supported encoding)
		//UTF7 encode folder names
		//if( in_array(strtoupper($command), array('COPY', 'CREATE', 'RENAME', 'DELETE')) )
        //	$tokens[0] = mb_convert_encoding($tokens[0], 'UTF7-IMAP', 'UTF-8');
		//if( in_array(strtoupper($command), array('RENAME')) )
        //	$tokens[1] = mb_convert_encoding($tokens[1], 'UTF7-IMAP', 'UTF-8');
		
        $line = $tag . ' ' . $appendToCommand . $command;
        $this->_previousCommand = '';
        foreach ($tokens as $token) {
			
			if (is_array($token)) {
			    
			    $this->_imapLog($line . ' ' . $token[0], 'C');
			    
			    $this->_previousCommand .= $line . ' ' . $token[0] . "\r\n";
				if (@fputs($this->_socket, $line . ' ' . $token[0] . "\r\n") === false) {
                    /**
                     * @see Atmail_Mail_Protocol_Exception
                     */
                    require_once 'Atmail/Mail/Protocol/Exception.php';
                    throw new Atmail_Mail_Protocol_Exception('cannot write - connection closed?');
                }
                if (!$this->_assumedNextLine('+ ')) {
                    /**
                     * @see Atmail_Mail_Protocol_Exception
                     */
                    require_once 'Atmail/Mail/Protocol/Exception.php';
                    throw new Atmail_Mail_Protocol_Exception('cannot send literal string!!');
                }
                $line = $token[1];
            } else {
                $line .= ' ' . $token;
            }
        }
        
        $this->_imapLog($line, 'C');
        
        $this->_previousCommand .= $line . "\r\n";
		if (@fputs($this->_socket, $line . "\r\n") === false) {
            /**
             * @see Atmail_Mail_Protocol_Exception
             */
            require_once 'Atmail/Mail/Protocol/Exception.php';
            throw new Atmail_Mail_Protocol_Exception('cannot write - connection closed?');
        }
    }

	public function readNestedResponse($tag, $dontParse = false)
    {
        $lines = array();
        while (!$this->readNestedLine($tokens, $tag, $dontParse)) {
            $lines[] = $tokens;
        }

        if ($dontParse) {
            // last to chars are still needed for response code
            $tokens = array(substr($tokens, 0, 2));
        }
        // last line has response code
        if ($tokens[0] == 'OK') {
            return $lines ? $lines : true;
        } else if ($tokens[0] == 'NO'){
            return false;
        }
        return null;
    }

	public function requestAndNestedResponse($command, $tokens = array(), $dontParse = false)
    {
        $this->sendRequest($command, $tokens, $tag);
        $response = $this->readNestedResponse($tag, $dontParse);

        return $response;
    } 



	
    /**
     * fetch one or more items of one or more messages
     * modifying fetch to not try match requested id against returned key but instead against the returned UID arg
     *
     * @param  string|array $items items to fetch from message(s) as string (if only one item)
     *                             or array of strings
     * @param  int          $fromUID  message for items or start message if $toUID !== null
     * @param  int|null     $toUID    if null only one message ($fromUID) is fetched, else it's the
     *                             last message, INF means last message avaible
     * @return string|array if only one item of one message is fetched it's returned as string
     *                      if items of one message are fetched it's returned as (name => value)
     *                      if one items of messages are fetched it's returned as (msgno => value)
     *                      if items of messages are fetchted it's returned as (msgno => (name => value))
     * @throws Atmail_Mail_Protocol_Exception
     */
    public function fetch($items, $fromUID, $toUID = null)
    {
		
		//N.B. $fromUID can be single id, a comma seperated string of ids (if $toUID == null), or an array of requested ids
		
		if (is_array($fromUID)) {
            $set = implode(',', $fromUID);
        } else if ($toUID === null) {
            $set = (int)$fromUID;
        } else if ($toUID === INF) {
            $set = (int)$fromUID . ':*';
        } else {
            $set = (int)$fromUID . ':' . (int)$toUID;
        }
		$this->_imapLog('set calculated as: ' . $set);        
		
        $items = (array)$items;
        $itemList = $this->escapeList($items);
        
        $this->sendRequest('FETCH', array($set, $itemList), $tag);
        
        $result = array();
		$a = 1;
        while (!$this->readLine($tokens, $tag)) {
            
			// ignore other responses
            if ($tokens[1] != 'FETCH') {
                continue;
            }
            // ignore other messages
            //this added to catch come setvers not responding with UID key but ordinal key e.g. IMail v11  
			if ( 
				$toUID === null &&	!is_array($fromUID) && 
				( 
					!(
						$tokens[2][1] == $fromUID || 
						(
							isset($tokens[2][2]) && 
							$tokens[2][2] == 'UID' && 
							isset($tokens[2][3]) && 
							$tokens[2][3] == $fromUID
						
						)
					)
			    ) 
			) {  //changed $tokens[0] (ordinal id) to $tokens[2][1] (uid token value)
                continue;
            }
            // if we only want one item we return that one directly
            if (count($items) == 1) {
                if ($tokens[2][0] == $items[0]) {
                    $data = $tokens[2][1];
                } else {
                    // maybe the server send an other field we didn't wanted
                    $count = count($tokens[2]);
                    // we start with 2, because 0 was already checked
                    for ($i = 2; $i < $count; $i += 2) {
                        if ($tokens[2][$i] != $items[0]) {
                            continue;
                        }
                        $data = $tokens[2][$i + 1];
                        break;
                    }
                }
            } else {
                $data = array();
                while (key($tokens[2]) !== null) {
                    $data[current($tokens[2])] = next($tokens[2]);
                    next($tokens[2]);
                }
            }
            // if we want only one message we can ignore everything else and just return
            if ($toUID === null && !is_array($fromUID) && 
				(
					$tokens[2][1] == $fromUID || 
					(
						isset($tokens[2][2]) && $tokens[2][2] == 'UID'
						&& 
						isset($tokens[2][3]) &&	$tokens[2][3] == $fromUID
					
					)
				)
			) {  //changed $tokens[0] (ordinal id) to $tokens[2][1] (uid token value)
                // we still need to read all lines
                
				while (!$this->readLine($tokens, $tag)) {
					
					//if we encounder a line starting with NO then we have an error
					if( isset($tokens[0]) && $tokens[0] == 'NO') {

						array_shift($tokens);
						throw new Atmail_Mail_Protocol_Exception( implode(' ', $tokens) );

					}
                
				}
				return $data;
            }
            $result[$tokens[0]] = $data;
        }
		
        if ($toUID === null && !is_array($fromUID)) {
            /**
             * @see Atmail_Mail_Protocol_Exception
             */
			require_once 'Atmail/Mail/Protocol/Exception.php';
            throw new Atmail_Mail_Protocol_Exception('Unable to find message with UID (' . $set . ') in current folder');
        }
        return $result;

    }

	/**
	 * because ZF in _readLine() tries to explode lines (not handling multiline responses properly) and recognise first part as the tag, it corrupts the reading of multiline responses, so implimente here to better handle responses.
	 * will read response looking for $tag OK then returns response
	 * will unpack * into array elements (multi message responses)
	*/
	public function _getEntireRawResponse( $tag ) {
		
		$multiMessageArray = array();
		$messageNumber = -1;
		
		while( $lastLine = @fgets($this->_socket) ) {     

			//if  current line starts with NO then error with previous item (Dovecot at least - need to test others)
			if( strpos($lastLine,'* NO') === 0 && $messageNumber > -1 ) {
				//de dont want these error lines contaminating the responses so continue (we already have a blank recponse line referencing the UID)
				//$multiMessageArray[$messageNumber] .= $lastLine;      
				continue;
			}
			
			//echo '<pre>' . __METHOD__ . ' #' . __LINE__ . ' : $lastLine <br />' . htmlentities(print_r($lastLine, true),null,'utf-8') . '</pre>';
			if( strpos($lastLine,'*') === 0 ) { //TODO: confirm that content with * on new line does not corrupt this
				$messageNumber++;              
				$lastLine = substr($lastLine,2);
			}
			
			
			if( strpos($lastLine, $tag . ' NO') === 0 || strpos($lastLine, $tag . ' BAD') === 0 ) {
				
				$this->_imapLog('Error received in response while raw reading tag:' . $tag . ': ' . $lastLine );
				if( $messageNumber == -1 )
				{
					
					Zend_Registry::get('log')->debug( "\n" . print_r($lastLine, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$lastLine \n");
					die(__METHOD__ . ' #' . __LINE__ . "Error received in response from server: " . $lastLine);
					
				}
				$multiMessageArray[$messageNumber] .= $lastLine;
				return $multiMessageArray;
			
			}
			if( strpos($lastLine, $tag . ' OK') === 0 ) {
				//echo '<pre>' . __METHOD__ . ' #' . __LINE__ . ' : $multiMessageArray DONE; <br />' . htmlentities(print_r($multiMessageArray, true),null,'utf-8') . '</pre>';
				
				return $multiMessageArray;                   
			}
		
			if(isset($multiMessageArray[$messageNumber]))
			{
				$multiMessageArray[$messageNumber] .= $lastLine;
			}
			else
			{
				$multiMessageArray[$messageNumber] = $lastLine;
			}	
		}
		
		return $multiMessageArray;                      
		
	}

	/**
     * fetch one or more items of one or more messages
     * modifying fetch to not try match requested id against returned key but instead against the returned UID arg
     *
     * @param  string|array $items items to fetch from message(s) as string (if only one item)
     *                             or array of strings
     * @param  int          $fromUID  message for items or start message if $toUID !== null
     * @param  int|null     $toUID    if null only one message ($fromUID) is fetched, else it's the
     *                             last message, INF means last message avaible
     * @return string|array if only one item of one message is fetched it's returned as string
     *                      if items of one message are fetched it's returned as (name => value)
     *                      if one items of messages are fetched it's returned as (msgno => value)
     *                      if items of messages are fetchted it's returned as (msgno => (name => value))
     * @throws Atmail_Mail_Protocol_Exception
     */
    public function nestedFetch($items, $fromUID, $toUID = null, $forceOrdinalIDs = false)
    {
		//N.B. $fromUID can be from, or single id (if $toUID == null), or an array of requested ids
		if (is_array($fromUID)) 
		{
            //solution to IMAP requests being longer than allowed: break into multiple calls to this method and returned array_merge
			$set = implode(',', $fromUID);                                                                                          
			if( strlen($set) > $this->_IMAPQueryMaxLength-500 )
			{
				
				//assume UID max strlen = 10
				$arrayDelta = round(($this->_IMAPQueryMaxLength-500)/10);
				$fromUIDChunks = array_chunk( $fromUID, $arrayDelta);
				$bigResult = array();
				foreach( $fromUIDChunks as &$fromUIDChunk )
					$bigResult = array_merge( $bigResult, $this->nestedFetch($items, $fromUIDChunk, null, $forceOrdinalIDs) );
				return $bigResult;
				
			}
			
        }
		elseif( $toUID === null )
            $set = (int)$fromUID;
        elseif( $toUID === INF )
            $set = (int)$fromUID . ':*';
        else
            $set = (int)$fromUID . ':' . (int)$toUID;


        $items = (array)$items;
        $itemList = $this->escapeList($items);
        $this->sendRequest('FETCH', array($set, $itemList), $tag, $forceOrdinalIDs);
		$rawResponse = $this->_getEntireRawResponse($tag);
			
		foreach( $rawResponse as $message ) 
		{
			
			$tokens = $this->_decodeNestedResponse($message);
			// ignore other responses
            if ($tokens[1] != 'FETCH')
                continue;

            // ignore other messages
            if ($toUID === null && !is_array($fromUID) && $tokens[2][1] != $fromUID) //changed $tokens[0] (ordinal id) to $tokens[2][1] (uid token value)
                continue;

            // if we only want one item we return that one directly
			if (count($items) == 1) 
			{
                //$this->log->debug( __METHOD__ . ' #' . __LINE__ . ": only searching for " . $items[0] );
                
				if ($tokens[2][0] == $items[0])
                    $data = $tokens[2][1];
                else 
				{
                
    				// maybe the server send an other field we didn't wanted
                    $count = count($tokens[2]);
                    // we start with 2, because 0 was already checked
                    for ($i = 2; $i < $count; $i += 2) 
					{

                        if ($tokens[2][$i] != $items[0]) 
						    continue;
                        $data = $tokens[2][$i + 1];
                        break;

                    }
                }
            }
			else
			{
				
				$data = array();
                while (key($tokens[2]) !== null) 
				{
                
    				$data[current($tokens[2])] = next($tokens[2]);
                    next($tokens[2]);

                }

            }
			
            // if we want only one message we can ignore everything else and just return
            if ($toUID === null && !is_array($fromUID) && $tokens[2][1] == $fromUID) 
			{  //changed $tokens[0] (ordinal id) to $tokens[2][1] (uid token value)
            	
				// we still need to read all lines
                while ( !$line = $this->readNestedLine($tokens, $tag) ) 
					$this->log->debug( __METHOD__ . ' #' . __LINE__ . ": \$line \n" . print_r($line, true) );
	            return $data;
            
			}
            $result[] = $data;
        }
		
        if ($toUID === null && !is_array($fromUID))
			throw new Atmail_Mail_Protocol_Exception('the single id was not found in response');

		// ok now some servers resond with results sorted in uid order so we need to repack the results in to required order if is_array($fromUID)
		if( is_array($fromUID) )
		{
			
			if( !isset($result) || (is_array($result) && !array_key_exists('UID', $result[0])) ) 
				throw new Exception('No uid in nested result from server - really needed for sorting results');
			
			$newResult = array();
			foreach( $fromUID as $UID ) 
			{
				if( !isset($result) )
				{
					
					Zend_Registry::get('log')->debug( "\n" . print_r($fromUID, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$fromUID \n");
					die(__METHOD__ . ' #' . __LINE__);
					
					
				}
				foreach( $result as $message )
				{
					if( $message['UID'] == $UID )
					{
						$newMessageUIDs[] = $message;
						break;
					}
				}				
			}
			$result = $newMessageUIDs;
		}
        return $result;
    }

    public function fastFetch($items, $UIDs)
    {
		
		//N.B. $fromUID can be single id, a comma seperated string of ids (if $toUID == null), or an array of requested ids
		//expects results to be on a line each, simple
		$set = implode(',', $UIDs);
        $itemList = $this->escapeList($items);
        
        $this->sendRequest('FETCH', array($set, $itemList), $tag);
        
        $result = array();
		while( !$this->readLine($line, $tag, true)) 
		{

            $tokenValueStartPositions = array();
			if( strpos( $line, 'FETCH') === false )
                continue;
			$previousToken = 'FETCH';
			
			$record = array();
			foreach( $items as &$item )
			{

				$tokenValueEndPositions[$previousToken] = strpos( $line, $item );
				$tokenValueStartPositions[$item] = $tokenValueEndPositions[$previousToken]+strlen($item)+1;
				$previousToken = $item;

			}
			//last token end pos
			$tokenValueEndPositions[$previousToken] = strlen($line);
			// now check that next token positions
			foreach( $items as $key )
				foreach( $tokenValueStartPositions as &$tokenValueStartPosition )
					if( $tokenValueStartPosition > $tokenValueStartPositions[$key] && $tokenValueStartPosition < $tokenValueEndPositions[$key] )
				    	$tokenValueEndPositions[$key] = $tokenValueStartPosition;
            //Zend_Registry::get('log')->debug( "\n" . print_r($tokenValueStartPositions, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$tokenValueStartPositions \n");
			//Zend_Registry::get('log')->debug( "\n" . print_r($tokenValueEndPositions, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$tokenValueEndPositions \n");
			
            
			foreach( $tokenValueStartPositions as $key => &$value )
			{
				
				//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": \$line: " . $line);
				//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": " . $key . ' = (' . $value . '-' . $tokenValueEndPositions[$key] . ') = ' . substr( $line, $value, $tokenValueEndPositions[$key]-$value));
				$record[$key] = trim(trim(substr($line, $value, $tokenValueEndPositions[$key]-$value)), ' "()');
				
			}
			//cleanup simple nested fields
			//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": FLAGS = |" . trim(trim( trim($record['FLAGS']), "()"),"()")."|" );
			
			if( isset($record['FLAGS']) )
				$record['FLAGS'] = explode( ' ', trim(trim( trim($record['FLAGS']), "()"),"()"));
				
			$result[$record['UID']] = $record;
        }
//Zend_Registry::get('log')->debug( "\n" . print_r($result, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$result \n");

		return $result;

    }


	public function readNestedLine(&$tokens = array(), $wantedTag = '*', $dontParse = false)
    {
		//$dontParse = true;
		$line = $this->_nextTaggedLine($tag);
		//$timeDelta = microtime(true);
		//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": about to call _decodeNestedLine\n");
		
		if (!$dontParse)
            $tokens = $this->_decodeNestedLine($line);
		else
            $tokens = $line;
        /*
		Zend_Registry::get('log')->debug( __METHOD__ . ": called _decodeNestedLine: " . round((microtime(true)-$timeDelta)*1000,1) . " milliseconds. highest=" . $this->higestDelta);
		if( (microtime(true)-$timeDelta)*1000 > $this->higestDelta )
		{
		
			$this->higestDelta = round((microtime(true)-$timeDelta)*1000,1);
			
		}
        */
		// if tag is wanted tag we might be at the end of a multiline response
        return $tag == $wantedTag;
    }

	public function _decodeNestedResponse($line) 
	{
	
		//$timeDelta = microtime(true);
		//Zend_Registry::get('log')->debug( __METHOD__ . ' #' . __LINE__ . ": about to call _decodeNestedLine\n");
		$result = $this->_decodeNestedLine($line);
		/*
		Zend_Registry::get('log')->debug( __METHOD__ . ": called _decodeNestedLine: " . round((microtime(true)-$timeDelta)*1000,1) . " milliseconds. highest=" . $this->higestDelta);
		if( (microtime(true)-$timeDelta)*1000 > $this->higestDelta )
		{
		
			$this->higestDelta = round((microtime(true)-$timeDelta)*1000,1);
			
		}
		*/
		return $result;
	
	}
	/**
	 * upgrade of the native ZF _decodeLine to enable successful parsing of nested response data
	 */
	public function _decodeNestedLine(&$line, $state = 0, &$startParsePos = 0) 
	{
	
		//recognised states:
		//0 main loop
		//1 unquoted (ended by space), 
		//2 quoted (started by " ended by " not preceeeded by /" excluding //" or ///")
		//3 roundBracketed (started by ( ended by balanced ) )
		//4 squareBracketed (started by [ ended by balanced ] ) may do some special handling for ease of use of data
		//5 curly brackets ( started by {x} x specifying how far to read ahead the var as a literal. if line not long enough fetch more lines out the buffer)
		//with some special handling of data in these cases in side
		//start processing findings into array, and if nested state found recurse
		$return = array();
		$lineC = strlen($line);
		//echo $lineC . "<br />\n";
        
						
		if( $state == 0) {
			while( $startParsePos < $lineC ) {
                
				//if( $line{$startParsePos} != ' ' )
					//echo '<hr />' . htmlentities($line,null,'UTF-8') . '<br />Decoding Response: now at, ' . $startParsePos .' start of: ' . htmlentities(substr($line, $startParsePos,10),null,'UTF-8');
				if( $line{$startParsePos} == ' ' ) {

					//skip space
					$startParsePos++;

				} elseif( $line{$startParsePos} == '"' ) {

					//starts with a "
					$return[] = $this->_decodeNestedLine($line, 2, $startParsePos);

				} elseif( $line{$startParsePos} == '(' ) {
                    
					//starts with a ( 
					$return[] = $this->_decodeNestedLine($line, 3, $startParsePos);

				} elseif( $line{$startParsePos} == '{' ) {

					//literal delimiter
					$return[] = $this->_decodeNestedLine($line, 5, $startParsePos);
					//length of line could have changed after fetching additional lines for literal content so we recompute $lineC
					$lineC = strlen($line);
				} else {

				   	//starts with something else
					$return[] = $this->_decodeNestedLine($line, 1, $startParsePos);

				}

			}
		    return $return;			

		} elseif( $state == 1 ) {

			
			$nextSpacePos 						= strpos($line, ' ', $startParsePos+1);
			$nextOpeningRoundBracketPos 		= strpos($line, '(', $startParsePos+1);
			$nextClosingRoundBracketPos 		= strpos($line, ')', $startParsePos+1);
			$nextOpeningSquareBracketPos 		= strpos($line, '[', $startParsePos+1);
			$nextClosingSquareBracketPos 		= strpos($line, ']', $startParsePos+1);
			$nextDoubleQuotePos 				= strpos($line, '"', $startParsePos+1);
			$nextOpeningCurleyBracketePosPos 	= strpos($line, '{', $startParsePos+1);

			$nextRelevant = 'none';
			$nextRelevantPos = $lineC;

			if( $nextSpacePos !== false ) {

				$nextRelevant = 'space';
				$nextRelevantPos = $nextSpacePos;

			}


			if( $nextOpeningRoundBracketPos !== false && $nextOpeningRoundBracketPos < $nextRelevantPos ) {

				$nextRelevant = 'openingRoundBracket';
				$nextRelevantPos = $nextOpeningRoundBracketPos;

			}

			if( $nextClosingRoundBracketPos !== false && $nextClosingRoundBracketPos < $nextRelevantPos ) {

				$nextRelevant = 'closingRoundBracket';
				$nextRelevantPos = $nextClosingRoundBracketPos;

			}


			if( $nextDoubleQuotePos !== false && $nextDoubleQuotePos < $nextRelevantPos ) {

				$nextRelevant = 'doubleQuote';
				$nextRelevantPos = $nextDoubleQuotePos;

			}

			if( $nextOpeningCurleyBracketePosPos !== false && $nextOpeningCurleyBracketePosPos < $nextRelevantPos ) {

				$nextRelevant = 'openingCurleyBracket';
				$nextRelevantPos = $nextOpeningCurleyBracketePosPos;

			}

			if( $nextClosingSquareBracketPos !== false && $nextClosingSquareBracketPos < $nextRelevantPos ) {

				$nextRelevant = 'closingSquareBracket';
				$nextRelevantPos = $nextClosingSquareBracketPos;

			}

			if( $nextOpeningSquareBracketPos !== false && $nextOpeningSquareBracketPos < $nextRelevantPos ) {

				//$nextRelevant = 'openingSquareBracket';
				//$nextRelevantPos = $nextOpeningSquareBracketPos;
				//special case. simply find the next closing square and that is your full string (nested arguments passed back in one chunk (like header chunk))
				$nextRelevant = 'closingSquareBracket';
				$nextRelevantPos = $nextClosingSquareBracketPos + 1;

			}
			$return = substr($line, $startParsePos, $nextRelevantPos - $startParsePos);
			$startParsePos = $nextRelevantPos;
			return $return;

		} elseif( $state == 2 ) {

			//simply find next unescaped doublequote and return string found
			$matchesFound = preg_match('/(?<!\\\)"(.*?)(?<!\\\)"/', $line, $m, PREG_OFFSET_CAPTURE, $startParsePos);
			if($matchesFound > 3)
				throw new Exception('Remote server returned a bad response');
				
			if( $matchesFound == 0 ) {

				$return = substr($line, $startParsePos);
				$startParsePos = $lineC;
				return $return;

			}

			$startParsePos = $m[1][1] + strlen($m[1][0]) + 1;
			return $m[1][0];

		} elseif( $state == 3) {
            
			
			//startsWith the ( char so proceed to next char
			$startParsePos++;
            // echo "testing: " . $line{$startParsePos} . "<br />\n";
			

			while( $startParsePos < $lineC ) {

				if( $line{$startParsePos} == ')' ) {

					//if the next char to be evaluated is a ) then $startParsePos++ and return
					$startParsePos++;
					return $return;

				} elseif( $line{$startParsePos} == ' ' ) {

					//skip space
					$startParsePos++;

				} elseif( $line{$startParsePos} == '"' ) {

					//starts with a "
					$return[] = $this->_decodeNestedLine($line, 2, $startParsePos);
					
				} elseif( $line{$startParsePos} == '(' ) {

					//starts with a (
					$return[] = $this->_decodeNestedLine($line, 3, $startParsePos);

				} elseif( $line{$startParsePos} == '{' ) {

					//starts with a {
					$return[] = $this->_decodeNestedLine($line, 5, $startParsePos);

				} else {

					//starts with something else
					$response = $this->_decodeNestedLine($line, 1, $startParsePos);
					//check for a ) in the responce if outside a []
					$posibleClosingRoundBracketPos = strpos($response, ')');
					$posibleOpeningSquareBracketPos = strpos($response, '[');
					if( $posibleClosingRoundBracketPos !== false && ($posibleOpeningSquareBracketPos === false || $posibleClosingRoundBracketPos < $posibleOpeningSquareBracketPos) ) {

						$response = substr($response, 0, $posibleClosingRoundBracketPos);
						//set position pointer back to end of closing ]
						$startParsePos = $startParsePos - strlen($response) + $posibleClosingRoundBracketPos;
						$return[] = $response;

						return $return;

					} else {

						$return[] = $response;

					}

				}

			}
			return $return;

		} elseif( $state == 5 ) {

			$closingCurlyPos = strpos($line, '}', $startParsePos);
			//push startpos to end of linefeed
			//TODO: verify if other servers terminate lines differently to \r\n
             $lineBreakPos = strpos($line, "\r\n", $closingCurlyPos);
		   //echo ('found a curley at ' . $startParsePos . ':' . $closingCurlyPos . ':' . $lineBreakPos);
		   if( $lineBreakPos == $closingCurlyPos + 1 )
				$startLiteralPos = $closingCurlyPos + 3; //advance }\r\n
			else
				throw new Exception('illegal server response: no CRLF after literal delimiter');
			$literalLength = (int)substr($line, $startParsePos + 1, $closingCurlyPos - $startParsePos - 1);
			//echo '<pre>' . __METHOD__ . ' #' . __LINE__ . ' : $literalLength <br />' . print_r($literalLength, true) . '</pre>';
			
			$curleyLength = $closingCurlyPos - $startParsePos + 1; 
			while($lineC < $startLiteralPos + $literalLength ) {
				$line .= $this->_nextLine();                               
				$lineC = strlen($line);
			}
			$startParsePos = $startLiteralPos + $literalLength;
			
			return substr($line, $startLiteralPos, $literalLength);

		}
    
	}
	
	public function getQuota($namespace = "INBOX")
	{
		
		$response = $this->requestAndResponse('GETQUOTAROOT', array($this->escapeString($namespace)));
		if( !isset($response[1][2][1]) || !isset($response[1][2][2])
		||	!is_array($response[1]) || !is_array($response[1][2])
		||  !is_numeric($response[1][2][1]) || !is_numeric($response[1][2][2]))
		{
			
			return array(0, 0);
			
		}
		return array($response[1][2][1], $response[1][2][2]);
		
	}
	
	/**
     * customized to enable saving of delimiter for further use
     */
    public function listMailbox($reference = '', $mailbox = '*')
    {
        $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $mailbox));
        //$this->log->debug( "\n\n" . print_r($list, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$list \n\n");
        
		$result = array();
        if ( !$list || $list === true ) 
		{
        
    		return $result;
        
		}

        foreach ($list as $item) {
            //sanity check result for expected structure
			if (count($item) < 4 || $item[0] != 'LIST') {
                continue;
            }
            $result[$item[3]] = array('delim' => $item[2], 'flags' => $item[1]);
			//NOTICE: according to RFC different namespaces could have different delimiters
        }
		//$this->log->debug( "\n\n" . print_r($result, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$result \n\n");

        return $result;
    }

	
	/**
	 * upgraded from ZF protocol to handle array of uids in one IMAP command instead of requiring multiple individual IMAP commands (slow and heavy on big moves)
	 */

	public function store(array $flags, $from, $to = null, $mode = null, $silent = true)
    {
        $item = 'FLAGS';
        if ($mode == '+' || $mode == '-') {
            $item = $mode . $item;
        }
        if ($silent) {
            $item .= '.SILENT';
        }

        $flags = $this->escapeList($flags);
        

        if( is_array($from) )
			$from = implode(',', $from);
		$set = $from;
        if ($to != null)
            $set = (int)$from . ':' . ($to == INF ? '*' : (int)$to);
        
        $result = $this->requestAndResponse('UID STORE', array($set, $item, $flags), $silent);
        
        if ($silent) {
            return $result ? true : false;
        }

        $tokens = $result;
        $result = array();
        foreach ($tokens as $token) {
            if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') {
                continue;
            }
            $result[$token[0]] = $token[2][1];
        }

        return $result;
    }       

	public function copy($folder, $from, $to = null)
    {
        if( is_array($from) )
			$from = implode(',', $from);
		$set = $from;
        if ($to != null) {
            $set = (int)$from . ':' . ($to == INF ? '*' : (int)$to);
        }

        $result = $this->requestAndResponse('UID COPY', array($set, $this->escapeString($folder)), true);
		//TODO: for cache updating ned to return new uids. even for multiple messages in action
		
		return $result;
    }
	
    /**
     * get the next line from socket with error checking, but nothing else
     *
     * @return string next line
     * @throws Zend_Mail_Protocol_Exception
     */
    protected function _nextLine()
    {
        $line = @fgets($this->_socket);
        if ($line === false) {
            /**
             * @see Zend_Mail_Protocol_Exception
             */
            require_once 'Zend/Mail/Protocol/Exception.php';
            throw new Zend_Mail_Protocol_Exception('Server Error: cannot read - connection closed?');
        }
        
        $this->_imapLog($line, 'S');
        return $line;
    }
    
    private function _imapLog($string, $type = '') {
        
		try {
			Zend_Registry::get('log')->imap(" IMAP $type: " . trim($string) );
		} catch (Exception $e) { //we still want to log during shutdown
			if( defined('IMAP_LOG_PATH') )
				file_put_contents(IMAP_LOG_PATH , date('c') . " CLEANING UP IMAP $type: " . trim($string) . "\n", FILE_APPEND);	
		}
	}
	
	public function setCapability( $capability )
	{
		
		$this->_capability = $capability;
		
	}
	
	/**
     * Get capabilities from IMAP server
     *
     * @return array list of capabilities or of CAPABILITY not supported (old servers) the gracefully return array()
     * @throws Atmail_Mail_Protocol_Exception
     */
	public function getCapability()
    {
	
        if( isset($this->_capability) )
		{   
			
			return $this->_capability;
			
		}
		
		$this->_capability = array();
        
		$response = $this->requestAndResponse('CAPABILITY');
        			
        if ( !$response ) 
		{
        
    		return $this->_capability;

        }   
		
		//check if it is a nested array
		if( count($response) == 2 && is_array($response[0]) )
			$response = $response[0];
        
        $this->_capability = $response;
        
        return $this->_capability;
    
	}
	
	public function setNamespaces( $namespaces )
	{
		
		$this->_namespaces = $namespaces;
	
	}
	
	/**
     * Get the default namespace namespace from IMAP server (could have multiple)
     *
     * @return string namespace
     * @throws Atmail_Mail_Protocol_Exception
     */
	public function getFolderNamespaceDefault() 
	{
	
		$namespaces = $this->getNamespaces();
		//find first non NIL namespace and serve that
		foreach( $namespaces as $namespaceCagegory )
		{   
			
			if( is_array($namespaceCagegory) ) 
			{
				foreach( $namespaceCagegory as $namespace )
				{
			
					if( 
						is_array($namespace) && 
						array_key_exists(0, $namespace) && 
						array_key_exists(1, $namespace)
					  )
					{
		
						//TODO: currently stripping delimiter off until all dependant code updated to best practice of leaving delimiter on
						if( substr($namespace[0], -1) == $namespace[1] )
						{

							return substr($namespace[0], 0, -1);

						}
						else
						{

							return $namespace[0];

						}
			
					}
			
				}
			
			}
			
		}
		return '';
		
    }

	/**
     * Get namespace from IMAP server
     *
     * @return array list of namespaces with delimiters for each namespace (can differ)
     * @throws Atmail_Mail_Protocol_Exception
     */
	public function getNamespaces() 
	{
	
		if( isset( $this->_namespaces ) )
		{
			
			return $this->_namespaces;
		
		}
		
		$this->_namespaces = array(); //CONSIDER implications of rather returning [0][0][0] = '' and [0][0][1] = 'NIL'
		//check if NAMESPACE not in CAPABILITY return gracefully
		//if( !in_array('NAMESPACE', $this->getCapability() ) )
		//{
		//	return $this->_namespaces;
			
		//}   
		
		$response = $this->requestAndNestedResponse('NAMESPACE');

		//sanity check. handle the common response structure. (add different handling below for servers discovered to respond with different structure)
		if( is_array($response) && count($response) == 1 && $response[0][0] == 'NAMESPACE' )
		{
			//looks like we have a known/expected response
			array_shift($response[0]);
			$this->_namespaces = $response[0];
			return $this->_namespaces;
		}
		else
		{   
			
			// gracefully fail if server does not support NAMESPACE (unlikely)  
			Zend_Registry::get('log')->imap('unable to get or process NAMESPACE from server');
            return $this->_namespaces;                                              

		}
		
    }

	public function getFolderDelimiterDefault() 
	{
		
		$namespaces = $this->getNamespaces();
		//find first non NIL namespace and serve that
		foreach( $namespaces as $namespaceCagegory )
		{   
			
			if( is_array($namespaceCagegory) ) 
			{
				foreach( $namespaceCagegory as $namespace )
				{
			
					if( 
						is_array($namespace) && 
						array_key_exists(0, $namespace) && 
						array_key_exists(1, $namespace)
					  )
					{
		
						return $namespace[1];
			
					}
			
				}
			
			}
			
		}
		return 'NIL';   
				
	}
	
	
	/**
     * append a new message to given folder
     *
     * @param string $folder  name of target folder
     * @param string $message full message content
     * @param array  $flags   flags for new message
     * @param string $date    date for new message
     * @return bool success
     * @throws Zend_Mail_Protocol_Exception
     */
    public function append($folder, $message, $flags = null, $date = null)
    {
        $tokens = array();
        $tokens[] = $this->escapeString($folder);
        if ($flags !== null) {
            $tokens[] = $this->escapeList($flags);
        }
        if ($date !== null) {
            $tokens[] = $this->escapeString($date);
        }
        $tokens[] = $this->escapeString($message);

        return $this->requestAndResponse('APPEND', $tokens, true);        
    }
    
    /**
     * read all lines of response until given tag is found (last line of response)
     *
     * @param  string       $tag       the tag of your request
     * @param  string|array $filter    you can filter the response so you get only the
     *                                 given response lines
     * @param  bool         $dontParse if true every line is returned unparsed instead of
     *                                 the decoded tokens
     * @return null|bool|array tokens if success, false if error, null if bad request
     * @throws Zend_Mail_Protocol_Exception
     */
    public function readResponse($tag, $dontParse = false)
    {
        $lines = array();
        
        do {
			$res = $this->readLine($tokens, $tag, $dontParse);
            $lines[] = $tokens;
        } while (!$res);
        
        /*
        while (!$this->readLine($tokens, $tag, $dontParse)){
            $lines[] = $tokens;
        }
        */
        
        // Atmail mod (we need the response from the append command in order
        // to extract the uid)

        if (empty($lines) && !empty($tokens)) {
			$lines[] = $tokens;
		}

        if ($dontParse) {
            // last to chars are still needed for response code
            $tokens = array(substr($tokens, 0, 2));
        }
        // last line has response code
        if ($tokens[0] == 'OK') {
            return $lines ? $lines : true;
        } else if ($tokens[0] == 'NO'){
            return false;
        }
        return null;
    }


	public function checkCapabilityInResponse($line)
	{
		if(is_array($line))
			$line = $line[0];

		// check for capability string
		if (strpos($line, '[CAPABILITY') !== false)
		{
			$this->_imapLog('FOUND CAPS IN RESPONSE BODY');
			// * OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE STARTTLS AUTH=PLAIN AUTH=LOGIN]
			$capabilities = str_replace('* OK [', '', substr($line, 0, strpos($line, ']')));
			$this->_imapLog($capabilities);
			$this->setCapability(explode(' ', $capabilities));
		}
	}

	/**
	 * Open connection to IMAP server
	 *
	 * @param  string      $host  hostname or IP address of IMAP server
	 * @param  int|null    $port  of IMAP server, default is 143 (993 for ssl)
	 * @param  string|bool $ssl   use 'SSL', 'TLS' or false
	 * @return string welcome message
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function connect($host, $port = null, $ssl = false)
	{
		if ($ssl == 'SSL') {
			$host = 'ssl://' . $host;
		}

		if ($port === null) {
			$port = $ssl === 'SSL' ? 993 : 143;
		}

		$errno  =  0;
		$errstr = '';
		$this->_socket = @fsockopen($host, $port, $errno, $errstr, self::TIMEOUT_CONNECTION);
		if (!$this->_socket) {
			/**
			 * @see Zend_Mail_Protocol_Exception
			 */
			require_once 'Zend/Mail/Protocol/Exception.php';
			throw new Zend_Mail_Protocol_Exception('cannot connect to host; error = ' . $errstr .
				' (errno = ' . $errno . ' )');
		}

		$line = $this->_nextLine();

		$this->checkCapabilityInResponse($line);

		if (!strpos($line, '* OK') === 0) {
			/**
			 * @see Zend_Mail_Protocol_Exception
			 */
			require_once 'Zend/Mail/Protocol/Exception.php';
			throw new Zend_Mail_Protocol_Exception('host doesn\'t allow connection');
		}



		if ($ssl === 'TLS') {
			$result = $this->requestAndResponse('STARTTLS');
			$result = $result && stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
			if (!$result) {
				/**
				 * @see Zend_Mail_Protocol_Exception
				 */
				require_once 'Zend/Mail/Protocol/Exception.php';
				throw new Zend_Mail_Protocol_Exception('cannot enable TLS');
			}
		}
	}

	/**
	 * Login to IMAP server.
	 *
	 * @param  string $user      username
	 * @param  string $password  password
	 * @return bool success
	 * @throws Zend_Mail_Protocol_Exception
	 */
	public function login($user, $password)
	{
		$response = parent::login($user, $password);
		$this->checkCapabilityInResponse($response);
		return $response;
	}
}        

























































if (!defined('ATMAIL_USER_COUNT')) eval(base64_decode('ZGllKCdFcnJvcjogbGljZW5zaW5nIGVycm9yJyk7'));
