<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */

/**
 *
 *
 * PHP versions 4 and 5
 *
 * <pre>
 * +-----------------------------------------------------------------------+
 * |                                                                       |
 * | W3CŪ SOFTWARE NOTICE AND LICENSE                                      |
 * | http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231   |
 * |                                                                       |
 * | This work (and included software, documentation such as READMEs,      |
 * | or other related items) is being provided by the copyright holders    |
 * | under the following license. By obtaining, using and/or copying       |
 * | this work, you (the licensee) agree that you have read, understood,   |
 * | and will comply with the following terms and conditions.              |
 * |                                                                       |
 * | Permission to copy, modify, and distribute this software and its      |
 * | documentation, with or without modification, for any purpose and      |
 * | without fee or royalty is hereby granted, provided that you include   |
 * | the following on ALL copies of the software and documentation or      |
 * | portions thereof, including modifications:                            |
 * |                                                                       |
 * | 1. The full text of this NOTICE in a location viewable to users       |
 * |    of the redistributed or derivative work.                           |
 * |                                                                       |
 * | 2. Any pre-existing intellectual property disclaimers, notices,       |
 * |    or terms and conditions. If none exist, the W3C Software Short     |
 * |    Notice should be included (hypertext is preferred, text is         |
 * |    permitted) within the body of any redistributed or derivative      |
 * |    code.                                                              |
 * |                                                                       |
 * | 3. Notice of any changes or modifications to the files, including     |
 * |    the date changes were made. (We recommend you provide URIs to      |
 * |    the location from which the code is derived.)                      |
 * |                                                                       |
 * | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT    |
 * | HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED,    |
 * | INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR        |
 * | FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE    |
 * | OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS,           |
 * | COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.                               |
 * |                                                                       |
 * | COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT,        |
 * | SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE        |
 * | SOFTWARE OR DOCUMENTATION.                                            |
 * |                                                                       |
 * | The name and trademarks of copyright holders may NOT be used in       |
 * | advertising or publicity pertaining to the software without           |
 * | specific, written prior permission. Title to copyright in this        |
 * | software and any associated documentation will at all times           |
 * | remain with copyright holders.                                        |
 * |                                                                       |
 * +-----------------------------------------------------------------------+
 * </pre>
 *
 * @category   Net
 * @package    Net_NNTP
 * @author     Heino H. Gehlsen <heino@gehlsen.dk>
 * @copyright  2002-2011 Heino H. Gehlsen <heino@gehlsen.dk>. All Rights Reserved.
 * @license    http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 W3CŪ SOFTWARE NOTICE AND LICENSE
 * @version    SVN: $Id: Client.php 330426 2013-05-31 14:41:46Z janpascal $
 * @link       http://pear.php.net/package/Net_NNTP
 * @see
 */

// Warn about PHP bugs
if (version_compare(PHP_VERSION, '5.2.11') === 0) {
    trigger_error('PHP bug #16657 breaks feof() on socket streams! Connection consistency might be compromised!', E_USER_WARNING);
}

/**
 *
 */
require_once 'PEAR.php';
//require_once 'Net/NNTP/Error.php';
require_once 'Net/NNTP/Protocol/Responsecode.php';


// {{{ constants

/**
 * Default host
 *
 * @access     public
 * @ignore
 */
define('NET_NNTP_PROTOCOL_CLIENT_DEFAULT_HOST', 'localhost');

/**
 * Default port
 *
 * @access     public
 * @ignore
 */
define('NET_NNTP_PROTOCOL_CLIENT_DEFAULT_PORT', '119');

// }}}
// {{{ Net_NNTP_Protocol_Client

/**
 * Low level NNTP Client
 *
 * Implements the client part of the NNTP standard acording to:
 *  - RFC 977,
 *  - RFC 2980,
 *  - RFC 850/1036, and
 *  - RFC 822/2822
 *
 * Each NNTP command is represented by a method: cmd*()
 *
 * WARNING: The Net_NNTP_Protocol_Client class is considered an internal class
 *          (and should therefore currently not be extended directly outside of
 *          the Net_NNTP package). Therefore its API is NOT required to be fully
 *          stable, for as long as such changes doesn't affect the public API of
 *          the Net_NNTP_Client class, which is considered stable.
 *
 * TODO:	cmdListActiveTimes()
 *      	cmdDistribPats()
 *
 * @category   Net
 * @package    Net_NNTP
 * @author     Heino H. Gehlsen <heino@gehlsen.dk>
 * @version    package: 1.5.0 (stable)
 * @version    api: 0.9.0 (alpha)
 * @access     private
 * @see        Net_NNTP_Client
 */
class Net_NNTP_Protocol_Client extends PEAR
{
    // {{{ properties

    /**
     * The socket resource being used to connect to the NNTP server.
     *
     * @var resource
     * @access private
     */
    var $_socket = null;

    /**
     * Contains the last recieved status response code and text
     *
     * @var array
     * @access private
     */
    var $_currentStatusResponse = null;

    /**
     *
     *
     * @var     object
     * @access  private
     */
    var $_logger = null;

    // }}}
    // {{{ constructor

    /**
     * Constructor
     *
     * @access public
     */
    function Net_NNTP_Protocol_Client() {

    	//
//    	parent::PEAR('Net_NNTP_Error');
    	parent::PEAR();
    }

    // }}}
    // {{{ getPackageVersion()

    /**
     *
     *
     * @access public
     */
    function getPackageVersion() {
	return '1.5.0';
    }

    // }}}
    // {{{ getApiVersion()

    /**
     *
     *
     * @access public
     */
    function getApiVersion() {
	return '0.9.0';
    }

    // }}}
    // {{{ setLogger()

    /**
     *
     *
     * @param object $logger
     *
     * @access protected
     */
    function setLogger($logger)
    {
        $this->_logger = $logger;
    }

    // }}}
    // {{{ setDebug()

    /**
     * @deprecated
     */
    function setDebug($debug = true)
    {
    	trigger_error('You are using deprecated API v1.0 in Net_NNTP_Protocol_Client: setDebug() ! Debugging in now automatically handled when a logger is given.', E_USER_NOTICE);
    }

    // }}}
    // {{{ _sendCommand()

    /**
     * Send command
     *
     * Send a command to the server. A carriage return / linefeed (CRLF) sequence
     * will be appended to each command string before it is sent to the IMAP server.
     *
     * @param string $cmd The command to launch, ie: "ARTICLE 1004853"
     *
     * @return mixed (int) response code on success or (object) pear_error on failure
     * @access private
     */
    function _sendCommand($cmd)
    {
        // NNTP/RFC977 only allows command up to 512 (-2) chars.
        if (!strlen($cmd) > 510) {
            return $this->throwError('Failed writing to socket! (Command to long - max 510 chars)');
        }

/***************************************************************************************/
/* Credit: Thanks to Brendan Coles <bcoles@gmail.com> (http://itsecuritysolutions.org) */
/*         for pointing out possibility to inject pipelined NNTP commands into pretty  */
/*         much any Net_NNTP command-sending function with user input, by appending    */
/*         a new line character followed by the injection.                             */
/***************************************************************************************/
        // Prevent new line (and possible future) characters in the NNTP commands
        // Net_NNTP does not support pipelined commands. Inserting a new line charecter
        // allows sending multiple commands and thereby making the communication between
        // NET_NNTP and the server out of sync...
        if (preg_match_all('/\r?\n/', $cmd, $matches, PREG_PATTERN_ORDER)) {
            foreach ($matches[0] as $key => $match) {
                $this->_logger->debug("Illegal character in command: ". htmlentities(str_replace(array("\r","\n"), array("'Carriage Return'", "'New Line'"), $match)));
            }
            return $this->throwError("Illegal character(s) in NNTP command!");
        }

    	// Check if connected
    	if (!$this->_isConnected()) {
            return $this->throwError('Failed to write to socket! (connection lost!)');
        }

    	// Send the command
    	$R = @fwrite($this->_socket, $cmd . "\r\n");
        if ($R === false) {
            return $this->throwError('Failed to write to socket!');
        }

    	//
    	if ($this->_logger && $this->_logger->_isMasked(PEAR_LOG_DEBUG)) {
    	    $this->_logger->debug('C: ' . $cmd);
        }

    	//
    	return $this->_getStatusResponse();
    }

    // }}}
    // {{{ _getStatusResponse()

    /**
     * Get servers status response after a command.
     *
     * @return mixed (int) statuscode on success or (object) pear_error on failure
     * @access private
     */
    function _getStatusResponse()
    {
    	// Retrieve a line (terminated by "\r\n") from the server.
        // RFC says max is 510, but IETF says "be liberal in what you accept"...
    	$response = @fgets($this->_socket, 4096);
        if ($response === false) {
            return $this->throwError('Failed to read from socket...!');
        }

    	//
    	if ($this->_logger && $this->_logger->_isMasked(PEAR_LOG_DEBUG)) {
    	    $this->_logger->debug('S: ' . rtrim($response, "\r\n"));
        }

    	// Trim the start of the response in case of misplased whitespace (should not be needen!!!)
    	$response = ltrim($response);

        $this->_currentStatusResponse = array(
    	    	    	    	    	      (int) substr($response, 0, 3),
    	                                      (string) rtrim(substr($response, 4))
    	    	    	    	    	     );

    	//
    	return $this->_currentStatusResponse[0];
    }

    // }}}
    // {{{ _getTextResponse()

    /**
     * Retrieve textural data
     *
     * Get data until a line with only a '.' in it is read and return data.
     *
     * @return mixed (array) text response on success or (object) pear_error on failure
     * @access private
     */
    function _getTextResponse()
    {
        $data = array();
        $line = '';

    	//
    	$debug = $this->_logger && $this->_logger->_isMasked(PEAR_LOG_DEBUG);

        // Continue until connection is lost
        while (!feof($this->_socket)) {

            // Retrieve and append up to 1024 characters from the server.
            $recieved = @fgets($this->_socket, 1024);

            if ($recieved === false) {
                return $this->throwError('Failed to read line from socket.', null);
    	    }

            $line .= $recieved;

            // Continue if the line is not terminated by CRLF
            if (substr($line, -2) != "\r\n" || strlen($line) < 2) {
                continue;
            }

            // Validate recieved line
            if (false) {
                // Lines should/may not be longer than 998+2 chars (RFC2822 2.3)
                if (strlen($line) > 1000) {
    	    	    if ($this->_logger) {
    	    	    	$this->_logger->notice('Max line length...');
    	    	    }
                    return $this->throwError('Invalid line recieved!', null);
                }
            }

            // Remove CRLF from the end of the line
            $line = substr($line, 0, -2);

            // Check if the line terminates the textresponse
            if ($line == '.') {

    	    	if ($this->_logger) {
    	    	    $this->_logger->debug('T: ' . $line);
    	    	}

                // return all previous lines
                return $data;
            }

            // If 1st char is '.' it's doubled (NNTP/RFC977 2.4.1)
            if (substr($line, 0, 2) == '..') {
                $line = substr($line, 1);
            }

    	    //
    	    if ($debug) {
    	    	$this->_logger->debug('T: ' . $line);
    	    }

            // Add the line to the array of lines
            $data[] = $line;

            // Reset/empty $line
            $line = '';
        }

    	if ($this->_logger) {
    	    $this->_logger->warning('Broke out of reception loop! This souldn\'t happen unless connection has been lost?');
    	}

    	//
    	return $this->throwError('End of stream! Connection lost?', null);
    }

    // }}}
    // {{{ _sendText()

    /**
     *
     *
     * @access private
     */
    function _sendArticle($article)
    {
    	/* data should be in the format specified by RFC850 */

    	switch (true) {
    	case is_string($article):
    	    //
    	    @fwrite($this->_socket, $article);
    	    @fwrite($this->_socket, "\r\n.\r\n");

    	    //
    	    if ($this->_logger && $this->_logger->_isMasked(PEAR_LOG_DEBUG)) {
    	        foreach (explode("\r\n", $article) as $line) {
    		    $this->_logger->debug('D: ' . $line);
    	        }
    	    	$this->_logger->debug('D: .');
    	    }
	    break;

    	case is_array($article):
    	    //
    	    $header = reset($article);
    	    $body = next($article);

/* Experimental...
    	    // If header is an array, implode it.
    	    if (is_array($header)) {
    	        $header = implode("\r\n", $header) . "\r\n";
    	    }
*/

    	    // Send header (including separation line)
    	    @fwrite($this->_socket, $header);
    	    @fwrite($this->_socket, "\r\n");

    	    //
    	    if ($this->_logger && $this->_logger->_isMasked(PEAR_LOG_DEBUG)) {
    	        foreach (explode("\r\n", $header) as $line) {
    	    	    $this->_logger->debug('D: ' . $line);
    	    	}
    	    }


/* Experimental...
    	    // If body is an array, implode it.
    	    if (is_array($body)) {
    	        $header = implode("\r\n", $body) . "\r\n";
    	    }
*/

    	    // Send body
    	    @fwrite($this->_socket, $body);
    	    @fwrite($this->_socket, "\r\n.\r\n");

    	    //
    	    if ($this->_logger && $this->_logger->_isMasked(PEAR_LOG_DEBUG)) {
    	        foreach (explode("\r\n", $body) as $line) {
    	    	    $this->_logger->debug('D: ' . $line);
    	    	}
    	        $this->_logger->debug('D: .');
    	    }
	    break;

	default:
    	    return $this->throwError('Ups...', null, null);
    	}

	return true;
    }

    // }}}
    // {{{ _currentStatusResponse()

    /**
     *
     *
     * @return string status text
     * @access private
     */
    function _currentStatusResponse()
    {
    	return $this->_currentStatusResponse[1];
    }

    // }}}
    // {{{ _handleUnexpectedResponse()

    /**
     *
     *
     * @param int $code Status code number
     * @param string $text Status text
     *
     * @return mixed
     * @access private
     */
    function _handleUnexpectedResponse($code = null, $text = null)
    {
    	if ($code === null) {
    	    $code = $this->_currentStatusResponse[0];
	}

    	if ($text === null) {
    	    $text = $this->_currentStatusResponse();
	}

    	switch ($code) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NOT_PERMITTED: // 502, 'access restriction or permission denied' / service permanently unavailable
    	    	return $this->throwError('Command not permitted / Access restriction / Permission denied', $code, $text);
    	    	break;
    	    default:
    	    	return $this->throwError("Unexpected response: '$text'", $code, $text);
    	}
    }

    // }}}

/* Session administration commands */

    // {{{ Connect()

    /**
     * Connect to a NNTP server
     *
     * @param string	$host	(optional) The address of the NNTP-server to connect to, defaults to 'localhost'.
     * @param mixed	$encryption	(optional)
     * @param int	$port	(optional) The port number to connect to, defaults to 119.
     * @param int	$timeout	(optional)
     *
     * @return mixed (bool) on success (true when posting allowed, otherwise false) or (object) pear_error on failure
     * @access protected
     */
    function connect($host = null, $encryption = null, $port = null, $timeout = null)
    {
    	//
        if ($this->_isConnected() ) {
    	    return $this->throwError('Already connected, disconnect first!', null);
    	}

    	// v1.0.x API
    	if (is_int($encryption)) {
	    trigger_error('You are using deprecated API v1.0 in Net_NNTP_Protocol_Client: connect() !', E_USER_NOTICE);
    	    $port = $encryption;
	    $encryption = false;
    	}

    	//
    	if (is_null($host)) {
    	    $host = 'localhost';
    	}

    	// Choose transport based on encryption, and if no port is given, use default for that encryption
    	switch ($encryption) {
	    case null:
	    case false:
		$transport = 'tcp';
    	    	$port = is_null($port) ? 119 : $port;
		break;
	    case 'ssl':
	    case 'tls':
		$transport = $encryption;
    	    	$port = is_null($port) ? 563 : $port;
		break;
	    default:
    	    	trigger_error('$encryption parameter must be either tcp, tls or ssl.', E_USER_ERROR);
    	}

    	//
    	if (is_null($timeout)) {
    	    $timeout = 15;
    	}

    	// Open Connection
    	$R = stream_socket_client($transport . '://' . $host . ':' . $port, $errno, $errstr, $timeout);
    	if ($R === false) {
    	    if ($this->_logger) {
    	        $this->_logger->notice("Connection to $transport://$host:$port failed.");
    	    }
    	    return $R;
    	}

    	$this->_socket = $R;

    	//
    	if ($this->_logger) {
    	    $this->_logger->info("Connection to $transport://$host:$port has been established.");
    	}

    	// Retrive the server's initial response.
    	$response = $this->_getStatusResponse();
    	if (PEAR::isError($response)) {
    	    return $response;
        }

        switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_ALLOWED: // 200, Posting allowed
    	    	// TODO: Set some variable before return

    	        return true;
    	        break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_PROHIBITED: // 201, Posting NOT allowed
    	        //
    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Posting not allowed!');
    	    	}

	    	// TODO: Set some variable before return

    	    	return false;
    	        break;
    	    case 400:
    	    	return $this->throwError('Server refused connection', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NOT_PERMITTED: // 502, 'access restriction or permission denied' / service permanently unavailable
    	    	return $this->throwError('Server refused connection', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ disconnect()

    /**
     * alias for cmdQuit()
     *
     * @access protected
     */
    function disconnect()
    {
    	return $this->cmdQuit();
    }

    // }}}
    // {{{ cmdCapabilities()

    /**
     * Returns servers capabilities
     *
     * @return mixed (array) list of capabilities on success or (object) pear_error on failure
     * @access protected
     */
    function cmdCapabilities()
    {
        // tell the newsserver we want an article
        $response = $this->_sendCommand('CAPABILITIES');
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
            case NET_NNTP_PROTOCOL_RESPONSECODE_CAPABILITIES_FOLLOW: // 101, Draft: 'Capability list follows'
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}
    	    	return $data;
    	        break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdModeReader()

    /**
     *
     *
     * @return mixed (bool) true when posting allowed, false when postind disallowed or (object) pear_error on failure
     * @access protected
     */
    function cmdModeReader()
    {
        // tell the newsserver we want an article
        $response = $this->_sendCommand('MODE READER');
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
            case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_ALLOWED: // 200, RFC2980: 'Hello, you can post'

	    	// TODO: Set some variable before return

    	    	return true;
    	        break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_READY_POSTING_PROHIBITED: // 201, RFC2980: 'Hello, you can't post'
    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Posting not allowed!');
    	    	}

	    	// TODO: Set some variable before return

    	    	return false;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NOT_PERMITTED: // 502, 'access restriction or permission denied' / service permanently unavailable
    	    	return $this->throwError('Connection being closed, since service so permanently unavailable', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdQuit()

    /**
     * Disconnect from the NNTP server
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdQuit()
    {
    	// Tell the server to close the connection
    	$response = $this->_sendCommand('QUIT');
        if (PEAR::isError($response)) {
            return $response;
    	}

        switch ($response) {
    	    case 205: // RFC977: 'closing connection - goodbye!'
    	    	// If socket is still open, close it.
    	    	if ($this->_isConnected()) {
    	    	    fclose($this->_socket);
    	    	}

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Connection closed.');
    	    	}

    	    	return true;
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}

/* */

    // {{{ cmdStartTLS()

    /**
     *
     *
     * @return mixed (bool) on success or (object) pear_error on failure
     * @access protected
     */
    function cmdStartTLS()
    {
        $response = $this->_sendCommand('STARTTLS');
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case 382: // RFC4642: 'continue with TLS negotiation'
    	    	$encrypted = stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
    	    	switch (true) {
    	    	    case $encrypted === true:
    	    	    	if ($this->_logger) {
    	    	    	    $this->_logger->info('TLS encryption started.');
    	    	    	}
    	    	    	return true;
    	    	    	break;
    	    	    case $encrypted === true:
    	    	    	if ($this->_logger) {
    	    	    	    $this->_logger->info('TLS encryption failed.');
    	    	    	}
    	    	    	return $this->throwError('Could not initiate TLS negotiation', $response, $this->_currentStatusResponse());
    	    	    	break;
    	    	    case is_int($encrypted):
    	    	    	return $this->throwError('', $response, $this->_currentStatusResponse());
    	    	    	break;
    	    	    default:
    	    	    	return $this->throwError('Internal error - unknown response from stream_socket_enable_crypto()', $response, $this->_currentStatusResponse());
    	    	}
    	    	break;
    	    case 580: // RFC4642: 'can not initiate TLS negotiation'
    	    	return $this->throwError('', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}

/* Article posting and retrieval */

    /* Group and article selection */

    // {{{ cmdGroup()

    /**
     * Selects a news group (issue a GROUP command to the server)
     *
     * @param string $newsgroup The newsgroup name
     *
     * @return mixed (array) groupinfo on success or (object) pear_error on failure
     * @access protected
     */
    function cmdGroup($newsgroup)
    {
        $response = $this->_sendCommand('GROUP '.$newsgroup);
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_GROUP_SELECTED: // 211, RFC977: 'n f l s group selected'
    	    	$response_arr = explode(' ', trim($this->_currentStatusResponse()));

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Group selected: '.$response_arr[3]);
    	    	}

    	    	return array('group' => $response_arr[3],
    	                     'first' => $response_arr[1],
    	    	             'last'  => $response_arr[2],
    	                     'count' => $response_arr[0]);
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_GROUP: // 411, RFC977: 'no such news group'
    	    	return $this->throwError('No such news group', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdListgroup()

    /**
     *
     *
     * @param optional string $newsgroup
     * @param optional mixed $range
     *
     * @return optional mixed (array) on success or (object) pear_error on failure
     * @access protected
     */
    function cmdListgroup($newsgroup = null, $range = null)
    {
        if (is_null($newsgroup)) {
    	    $command = 'LISTGROUP';
    	} else {
    	    if (is_null($range)) {
    	        $command = 'LISTGROUP ' . $newsgroup;
    	    } else {
    	        $command = 'LISTGROUP ' . $newsgroup . ' ' . $range;
    	    }
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_GROUP_SELECTED: // 211, RFC2980: 'list of article numbers follow'

    	    	$articles = $this->_getTextResponse();
    	        if (PEAR::isError($articles)) {
    	            return $articles;
    	        }

    	        $response_arr = explode(' ', trim($this->_currentStatusResponse()), 4);

		// If server does not return group summary in status response, return null'ed array
    	    	if (!is_numeric($response_arr[0]) || !is_numeric($response_arr[1]) || !is_numeric($response_arr[2]) || empty($response_arr[3])) {
    	    	    return array('group'    => null,
    	        	         'first'    => null,
    	    	    	         'last'     => null,
    	    	    		 'count'    => null,
    	    	    	         'articles' => $articles);
		}

    	    	return array('group'    => $response_arr[3],
    	                     'first'    => $response_arr[1],
    	    	             'last'     => $response_arr[2],
    	    	             'count'    => $response_arr[0],
    	    	             'articles' => $articles);
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC2980: 'Not currently in newsgroup'
    	    	return $this->throwError('Not currently in newsgroup', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 502: // RFC2980: 'no permission'
    	    	return $this->throwError('No permission', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdLast()

    /**
     *
     *
     * @return mixed (array) or (string) or (int) or (object) pear_error on failure
     * @access protected
     */
    function cmdLast()
    {
        //
        $response = $this->_sendCommand('LAST');
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_ARTICLE_SELECTED: // 223, RFC977: 'n a article retrieved - request text separately (n = article number, a = unique article id)'
    	    	$response_arr = explode(' ', trim($this->_currentStatusResponse()));

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Selected previous article: ' . $response_arr[0] .' - '. $response_arr[1]);
    	    	}

    	    	return array($response_arr[0], (string) $response_arr[1]);
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC977: 'no newsgroup selected'
    	    	return $this->throwError('No newsgroup has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC977: 'no current article has been selected'
    	    	return $this->throwError('No current article has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_PREVIOUS_ARTICLE: // 422, RFC977: 'no previous article in this group'
    	    	return $this->throwError('No previous article in this group', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdNext()

    /**
     *
     *
     * @return mixed (array) or (string) or (int) or (object) pear_error on failure
     * @access protected
     */
    function cmdNext()
    {
        //
        $response = $this->_sendCommand('NEXT');
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_ARTICLE_SELECTED: // 223, RFC977: 'n a article retrieved - request text separately (n = article number, a = unique article id)'
    	    	$response_arr = explode(' ', trim($this->_currentStatusResponse()));

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Selected previous article: ' . $response_arr[0] .' - '. $response_arr[1]);
    	    	}

    	    	return array($response_arr[0], (string) $response_arr[1]);
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC977: 'no newsgroup selected'
    	    	return $this->throwError('No newsgroup has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC977: 'no current article has been selected'
    	    	return $this->throwError('No current article has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_NEXT_ARTICLE: // 421, RFC977: 'no next article in this group'
    	    	return $this->throwError('No next article in this group', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}

    /* Retrieval of articles and article sections */

    // {{{ cmdArticle()

    /**
     * Get an article from the currently open connection.
     *
     * @param mixed $article Either a message-id or a message-number of the article to fetch. If null or '', then use current article.
     *
     * @return mixed (array) article on success or (object) pear_error on failure
     * @access protected
     */
    function cmdArticle($article = null)
    {
        if (is_null($article)) {
    	    $command = 'ARTICLE';
    	} else {
            $command = 'ARTICLE ' . $article;
        }

        // tell the newsserver we want an article
        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_ARTICLE_FOLLOWS:  // 220, RFC977: 'n <a> article retrieved - head and body follow (n = article number, <a> = message-id)'
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}

    	    	if ($this->_logger) {
    	    	    $this->_logger->info(($article == null ? 'Fetched current article' : 'Fetched article: '.$article));
    	    	}
    	    	return $data;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC977: 'no newsgroup has been selected'
    	    	return $this->throwError('No newsgroup has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC977: 'no current article has been selected'
    	    	return $this->throwError('No current article has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_NUMBER: // 423, RFC977: 'no such article number in this group'
    	    	return $this->throwError('No such article number in this group', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_ID: // 430, RFC977: 'no such article found'
    	    	return $this->throwError('No such article found', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdHead()

    /**
     * Get the headers of an article from the currently open connection.
     *
     * @param mixed $article Either a message-id or a message-number of the article to fetch the headers from. If null or '', then use current article.
     *
     * @return mixed (array) headers on success or (object) pear_error on failure
     * @access protected
     */
    function cmdHead($article = null)
    {
        if (is_null($article)) {
    	    $command = 'HEAD';
    	} else {
            $command = 'HEAD ' . $article;
        }

        // tell the newsserver we want the header of an article
        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_HEAD_FOLLOWS:     // 221, RFC977: 'n <a> article retrieved - head follows'
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}

    	    	if ($this->_logger) {
    	    	    $this->_logger->info(($article == null ? 'Fetched current article header' : 'Fetched article header for article: '.$article));
    	    	}

    	        return $data;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC977: 'no newsgroup has been selected'
    	    	return $this->throwError('No newsgroup has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC977: 'no current article has been selected'
    	    	return $this->throwError('No current article has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_NUMBER: // 423, RFC977: 'no such article number in this group'
    	    	return $this->throwError('No such article number in this group', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_ID: // 430, RFC977: 'no such article found'
    	    	return $this->throwError('No such article found', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdBody()

    /**
     * Get the body of an article from the currently open connection.
     *
     * @param mixed $article Either a message-id or a message-number of the article to fetch the body from. If null or '', then use current article.
     *
     * @return mixed (array) body on success or (object) pear_error on failure
     * @access protected
     */
    function cmdBody($article = null)
    {
        if (is_null($article)) {
    	    $command = 'BODY';
    	} else {
            $command = 'BODY ' . $article;
        }

        // tell the newsserver we want the body of an article
        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_BODY_FOLLOWS:     // 222, RFC977: 'n <a> article retrieved - body follows'
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}

    	    	if ($this->_logger) {
    	    	    $this->_logger->info(($article == null ? 'Fetched current article body' : 'Fetched article body for article: '.$article));
    	    	}

    	        return $data;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC977: 'no newsgroup has been selected'
    	    	return $this->throwError('No newsgroup has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC977: 'no current article has been selected'
    	    	return $this->throwError('No current article has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_NUMBER: // 423, RFC977: 'no such article number in this group'
    	    	return $this->throwError('No such article number in this group', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_ID: // 430, RFC977: 'no such article found'
    	    	return $this->throwError('No such article found', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdStat

    /**
     *
     *
     * @param mixed $article
     *
     * @return mixed (array) or (string) or (int) or (object) pear_error on failure
     * @access protected
     */
    function cmdStat($article = null)
    {
        if (is_null($article)) {
    	    $command = 'STAT';
    	} else {
            $command = 'STAT ' . $article;
        }

        // tell the newsserver we want an article
        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_ARTICLE_SELECTED: // 223, RFC977: 'n <a> article retrieved - request text separately' (actually not documented, but copied from the ARTICLE command)
    	    	$response_arr = explode(' ', trim($this->_currentStatusResponse()));

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Selected article: ' . $response_arr[0].' - '.$response_arr[1]);
    	    	}

    	    	return array($response_arr[0], (string) $response_arr[1]);
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC977: 'no newsgroup has been selected' (actually not documented, but copied from the ARTICLE command)
    	    	return $this->throwError('No newsgroup has been selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_NUMBER: // 423, RFC977: 'no such article number in this group' (actually not documented, but copied from the ARTICLE command)
    	    	return $this->throwError('No such article number in this group', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_ID: // 430, RFC977: 'no such article found' (actually not documented, but copied from the ARTICLE command)
    	    	return $this->throwError('No such article found', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}

    /* Article posting */

    // {{{ cmdPost()

    /**
     * Post an article to a newsgroup.
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdPost()
    {
        // tell the newsserver we want to post an article
    	$response = $this->_sendCommand('POST');
    	if (PEAR::isError($response)) {
    	    return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_SEND: // 340, RFC977: 'send article to be posted. End with <CR-LF>.<CR-LF>'
    	    	return true;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_PROHIBITED: // 440, RFC977: 'posting not allowed'
    	    	return $this->throwError('Posting not allowed', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}

    }

    // }}}
    // {{{ cmdPost2()

    /**
     * Post an article to a newsgroup.
     *
     * @param mixed $article (string/array)
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdPost2($article)
    {
    	/* should be presented in the format specified by RFC850 */

    	//
    	$this->_sendArticle($article);

    	// Retrive server's response.
    	$response = $this->_getStatusResponse();
    	if (PEAR::isError($response)) {
    	    return $response;
    	}

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_SUCCESS: // 240, RFC977: 'article posted ok'
    	    	return true;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_POSTING_FAILURE: // 441, RFC977: 'posting failed'
    	    	return $this->throwError('Posting failed', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdIhave()

    /**
     *
     *
     * @param string $id
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdIhave($id)
    {
        // tell the newsserver we want to post an article
    	$response = $this->_sendCommand('IHAVE ' . $id);
    	if (PEAR::isError($response)) {
    	    return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_SEND: // 335
    	    	return true;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_UNWANTED: // 435
    	    	return $this->throwError('Article not wanted', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_FAILURE: // 436
    	    	return $this->throwError('Transfer not possible; try again later', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdIhave2()

    /**
     *
     *
     * @param mixed $article (string/array)
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdIhave2($article)
    {
    	/* should be presented in the format specified by RFC850 */

    	//
    	$this->_sendArticle($article);

    	// Retrive server's response.
    	$response = $this->_getStatusResponse();
    	if (PEAR::isError($response)) {
    	    return $response;
    	}

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_SUCCESS: // 235
    	    	return true;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_FAILURE: // 436
    	    	return $this->throwError('Transfer not possible; try again later', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_TRANSFER_REJECTED: // 437
    	    	return $this->throwError('Transfer rejected; do not retry', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}

/* Information commands */

    // {{{ cmdDate()

    /**
     * Get the date from the newsserver format of returned date
     *
     * @return mixed (string) 'YYYYMMDDhhmmss' / (int) timestamp on success or (object) pear_error on failure
     * @access protected
     */
    function cmdDate()
    {
        $response = $this->_sendCommand('DATE');
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_SERVER_DATE: // 111, RFC2980: 'YYYYMMDDhhmmss'
    	        return $this->_currentStatusResponse();
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }
    // }}}
    // {{{ cmdHelp()

    /**
     * Returns the server's help text
     *
     * @return mixed (array) help text on success or (object) pear_error on failure
     * @access protected
     */
    function cmdHelp()
    {
        // tell the newsserver we want an article
        $response = $this->_sendCommand('HELP');
        if (PEAR::isError($response)) {
            return $response;
        }

    	switch ($response) {
            case NET_NNTP_PROTOCOL_RESPONSECODE_HELP_FOLLOWS: // 100
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}
    	    	return $data;
    	        break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdNewgroups()

    /**
     * Fetches a list of all newsgroups created since a specified date.
     *
     * @param int $time Last time you checked for groups (timestamp).
     * @param optional string $distributions (deprecaded in rfc draft)
     *
     * @return mixed (array) nested array with informations about existing newsgroups on success or (object) pear_error on failure
     * @access protected
     */
    function cmdNewgroups($time, $distributions = null)
    {
	$date = gmdate('ymd His', $time);

        if (is_null($distributions)) {
    	    $command = 'NEWGROUPS ' . $date . ' GMT';
    	} else {
    	    $command = 'NEWGROUPS ' . $date . ' GMT <' . $distributions . '>';
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NEW_GROUPS_FOLLOW: // 231, REF977: 'list of new newsgroups follows'
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}

    	    	$groups = array();
    	    	foreach($data as $line) {
    	    	    $arr = explode(' ', trim($line));

    	    	    $group = array('group'   => $arr[0],
    	    	                   'last'    => $arr[1],
    	    	                   'first'   => $arr[2],
    	    	                   'posting' => $arr[3]);

    	    	    $groups[$group['group']] = $group;
    	    	}
    	        return $groups;



    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdNewnews()

    /**
     *
     *
     * @param timestamp $time
     * @param mixed $newsgroups (string or array of strings)
     * @param mixed $distribution (string or array of strings)
     *
     * @return mixed
     * @access protected
     */
    function cmdNewnews($time, $newsgroups, $distribution = null)
    {
        $date = gmdate('ymd His', $time);

    	if (is_array($newsgroups)) {
    	    $newsgroups = implode(',', $newsgroups);
    	}

        if (is_null($distribution)) {
    	    $command = 'NEWNEWS ' . $newsgroups . ' ' . $date . ' GMT';
    	} else {
    	    if (is_array($distribution)) {
    		$distribution = implode(',', $distribution);
    	    }

    	    $command = 'NEWNEWS ' . $newsgroups . ' ' . $date . ' GMT <' . $distribution . '>';
        }

	// TODO: the lenght of the request string may not exceed 510 chars

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NEW_ARTICLES_FOLLOW: // 230, RFC977: 'list of new articles by message-id follows'
    	    	$messages = array();
    	    	foreach($this->_getTextResponse() as $line) {
    	    	    $messages[] = $line;
    	    	}
    	    	return $messages;
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}

    /* The LIST commands */

    // {{{ cmdList()

    /**
     * Fetches a list of all avaible newsgroups
     *
     * @return mixed (array) nested array with informations about existing newsgroups on success or (object) pear_error on failure
     * @access protected
     */
    function cmdList()
    {
        $response = $this->_sendCommand('LIST');
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_GROUPS_FOLLOW: // 215, RFC977: 'list of newsgroups follows'
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}

    	    	$groups = array();
    	    	foreach($data as $line) {
    	    	    $arr = explode(' ', trim($line));

    	    	    $group = array('group'   => $arr[0],
    	    	                   'last'    => $arr[1],
    	    	                   'first'   => $arr[2],
    	    	                   'posting' => $arr[3]);

    	    	    $groups[$group['group']] = $group;
    	    	}
    	        return $groups;
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdListActive()

    /**
     * Fetches a list of all avaible newsgroups
     *
     * @param string $wildmat
     *
     * @return mixed (array) nested array with informations about existing newsgroups on success or (object) pear_error on failure
     * @access protected
     */
    function cmdListActive($wildmat = null)
    {
        if (is_null($wildmat)) {
    	    $command = 'LIST ACTIVE';
    	} else {
            $command = 'LIST ACTIVE ' . $wildmat;
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_GROUPS_FOLLOW: // 215, RFC977: 'list of newsgroups follows'
    	    	$data = $this->_getTextResponse();
    	    	if (PEAR::isError($data)) {
    	    	    return $data;
    	    	}

    	    	$groups = array();
    	    	foreach($data as $line) {
    	    	    $arr = explode(' ', trim($line));

    	    	    $group = array('group'   => $arr[0],
    	    	                   'last'    => $arr[1],
    	    	                   'first'   => $arr[2],
    	    	                   'posting' => $arr[3]);

    	    	    $groups[$group['group']] = $group;
    	    	}

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Fetched list of available groups');
    	    	}

    	        return $groups;
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdListNewsgroups()

    /**
     * Fetches a list of (all) avaible newsgroup descriptions.
     *
     * @param string $wildmat Wildmat of the groups, that is to be listed, defaults to null;
     *
     * @return mixed (array) nested array with description of existing newsgroups on success or (object) pear_error on failure
     * @access protected
     */
    function cmdListNewsgroups($wildmat = null)
    {
        if (is_null($wildmat)) {
    	    $command = 'LIST NEWSGROUPS';
    	} else {
            $command = 'LIST NEWSGROUPS ' . $wildmat;
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_GROUPS_FOLLOW: // 215, RFC2980: 'information follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	    	$groups = array();

    	        foreach($data as $line) {
    	            if (preg_match("/^(\S+)\s+(.*)$/", ltrim($line), $matches)) {
    	    	        $groups[$matches[1]] = (string) $matches[2];
    	    	    } else {
    	    	        if ($this->_logger) {
    	    	            $this->_logger->warning("Recieved non-standard line: '$line'");
    	    	        }
    	    	    }
    	        }

    	        if ($this->_logger) {
    	            $this->_logger->info('Fetched group descriptions');
    	        }

    	        return $groups;
    		break;
    	    case 503: // RFC2980: 'program error, function not performed'
    	    	return $this->throwError('Internal server error, function not performed', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}

/* Article field access commands */

    // {{{ cmdOver()

    /**
     * Fetch message header from message number $first until $last
     *
     * The format of the returned array is:
     * $messages[][header_name]
     *
     * @param optional string $range articles to fetch
     *
     * @return mixed (array) nested array of message and there headers on success or (object) pear_error on failure
     * @access protected
     */
    function cmdOver($range = null)
    {
        if (is_null($range)) {
	    $command = 'OVER';
    	} else {
    	    $command = 'OVER ' . $range;
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_OVERVIEW_FOLLOWS: // 224, RFC2980: 'Overview information follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	        foreach ($data as $key => $value) {
    	            $data[$key] = explode("\t", trim($value));
    	        }

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Fetched overview ' . ($range == null ? 'for current article' : 'for range: '.$range));
    	    	}

    	    	return $data;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC2980: 'No news group current selected'
    	    	return $this->throwError('No news group current selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC2980: 'No article(s) selected'
    	    	return $this->throwError('No article(s) selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_SUCH_ARTICLE_NUMBER: // 423:, Draft27: 'No articles in that range'
    	    	return $this->throwError('No articles in that range', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 502: // RFC2980: 'no permission'
    	    	return $this->throwError('No permission', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdXOver()

    /**
     * Fetch message header from message number $first until $last
     *
     * The format of the returned array is:
     * $messages[message_id][header_name]
     *
     * @param optional string $range articles to fetch
     *
     * @return mixed (array) nested array of message and there headers on success or (object) pear_error on failure
     * @access protected
     */
    function cmdXOver($range = null)
    {
	// deprecated API (the code _is_ still in alpha state)
    	if (func_num_args() > 1 ) {
    	    die('The second parameter in cmdXOver() has been deprecated! Use x-y instead...');
        }

        if (is_null($range)) {
	    $command = 'XOVER';
    	} else {
    	    $command = 'XOVER ' . $range;
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_OVERVIEW_FOLLOWS: // 224, RFC2980: 'Overview information follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	        foreach ($data as $key => $value) {
    	            $data[$key] = explode("\t", trim($value));
    	        }

    	    	if ($this->_logger) {
    	    	    $this->_logger->info('Fetched overview ' . ($range == null ? 'for current article' : 'for range: '.$range));
    	    	}

    	    	return $data;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC2980: 'No news group current selected'
    	    	return $this->throwError('No news group current selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC2980: 'No article(s) selected'
    	    	return $this->throwError('No article(s) selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 502: // RFC2980: 'no permission'
    	    	return $this->throwError('No permission', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdListOverviewFmt()

    /**
     * Returns a list of avaible headers which are send from newsserver to client for every news message
     *
     * @return mixed (array) of header names on success or (object) pear_error on failure
     * @access protected
     */
    function cmdListOverviewFmt()
    {
    	$response = $this->_sendCommand('LIST OVERVIEW.FMT');
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_GROUPS_FOLLOW: // 215, RFC2980: 'information follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	        $format = array();

    	        foreach ($data as $line) {

		    // Check if postfixed by ':full' (case-insensitive)
		    if (0 == strcasecmp(substr($line, -5, 5), ':full')) {
    	    		// ':full' is _not_ included in tag, but value set to true
    	    		$format[substr($line, 0, -5)] = true;
		    } else {
    	    		// ':' is _not_ included in tag; value set to false
    	    		$format[substr($line, 0, -1)] = false;
    	            }
    	        }

    	        if ($this->_logger) {
    	            $this->_logger->info('Fetched overview format');
    	        }
    	        return $format;
    	    	break;
    	    case 503: // RFC2980: 'program error, function not performed'
    	    	return $this->throwError('Internal server error, function not performed', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdXHdr()

    /**
     *
     *
     * The format of the returned array is:
     * $messages[message_id]
     *
     * @param optional string $field
     * @param optional string $range articles to fetch
     *
     * @return mixed (array) nested array of message and there headers on success or (object) pear_error on failure
     * @access protected
     */
    function cmdXHdr($field, $range = null)
    {
        if (is_null($range)) {
	    $command = 'XHDR ' . $field;
    	} else {
    	    $command = 'XHDR ' . $field . ' ' . $range;
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case 221: // 221, RFC2980: 'Header follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	    	$return = array();
    	        foreach($data as $line) {
    	    	    $line = explode(' ', trim($line), 2);
    	    	    $return[$line[0]] = $line[1];
    	        }

    	    	return $return;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC2980: 'No news group current selected'
    	    	return $this->throwError('No news group current selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC2980: 'No current article selected'
    	    	return $this->throwError('No current article selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 430: // 430, RFC2980: 'No such article'
    	    	return $this->throwError('No such article', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 502: // RFC2980: 'no permission'
    	    	return $this->throwError('No permission', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}


















    /**
     * Fetches a list of (all) avaible newsgroup descriptions.
     * Depresated as of RFC2980.
     *
     * @param string $wildmat Wildmat of the groups, that is to be listed, defaults to '*';
     *
     * @return mixed (array) nested array with description of existing newsgroups on success or (object) pear_error on failure
     * @access protected
     */
    function cmdXGTitle($wildmat = '*')
    {
        $response = $this->_sendCommand('XGTITLE '.$wildmat);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case 282: // RFC2980: 'list of groups and descriptions follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	    	$groups = array();

    	        foreach($data as $line) {
    	            preg_match("/^(.*?)\s(.*?$)/", trim($line), $matches);
    	            $groups[$matches[1]] = (string) $matches[2];
    	        }

    	        return $groups;
    	    	break;

    	    case 481: // RFC2980: 'Groups and descriptions unavailable'
    	    	return $this->throwError('Groups and descriptions unavailable', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdXROver()

    /**
     * Fetch message references from message number $first to $last
     *
     * @param optional string $range articles to fetch
     *
     * @return mixed (array) assoc. array of message references on success or (object) pear_error on failure
     * @access protected
     */
    function cmdXROver($range = null)
    {
	// Warn about deprecated API (the code _is_ still in alpha state)
    	if (func_num_args() > 1 ) {
    	    die('The second parameter in cmdXROver() has been deprecated! Use x-y instead...');
    	}

        if (is_null($range)) {
    	    $command = 'XROVER';
    	} else {
    	    $command = 'XROVER ' . $range;
        }

        $response = $this->_sendCommand($command);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_OVERVIEW_FOLLOWS: // 224, RFC2980: 'Overview information follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	    	$return = array();
    	        foreach($data as $line) {
    	    	    $line = explode(' ', trim($line), 2);
    	    	    $return[$line[0]] = $line[1];
    	        }
    	    	return $return;
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_GROUP_SELECTED: // 412, RFC2980: 'No news group current selected'
    	    	return $this->throwError('No news group current selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case NET_NNTP_PROTOCOL_RESPONSECODE_NO_ARTICLE_SELECTED: // 420, RFC2980: 'No article(s) selected'
    	    	return $this->throwError('No article(s) selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 502: // RFC2980: 'no permission'
    	    	return $this->throwError('No permission', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}




    // {{{ cmdXPat()

    /**
     *
     *
     * @param string $field
     * @param string $range
     * @param mixed $wildmat
     *
     * @return mixed (array) nested array of message and there headers on success or (object) pear_error on failure
     * @access protected
     */
    function cmdXPat($field, $range, $wildmat)
    {
        if (is_array($wildmat)) {
	    $wildmat = implode(' ', $wildmat);
    	}

        $response = $this->_sendCommand('XPAT ' . $field . ' ' . $range . ' ' . $wildmat);
        if (PEAR::isError($response)){
            return $response;
        }

    	switch ($response) {
    	    case 221: // 221, RFC2980: 'Header follows'
    	    	$data = $this->_getTextResponse();
    	        if (PEAR::isError($data)) {
    	            return $data;
    	        }

    	    	$return = array();
    	        foreach($data as $line) {
    	    	    $line = explode(' ', trim($line), 2);
    	    	    $return[$line[0]] = $line[1];
    	        }

    	    	return $return;
    	    	break;
    	    case 430: // 430, RFC2980: 'No such article'
    	    	return $this->throwError('No current article selected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 502: // RFC2980: 'no permission'
    	    	return $this->throwError('No permission', $response, $this->_currentStatusResponse());
    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdAuthinfo()

    /**
     * Authenticate using 'original' method
     *
     * @param string $user The username to authenticate as.
     * @param string $pass The password to authenticate with.
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdAuthinfo($user, $pass)
    {
    	// Send the username
        $response = $this->_sendCommand('AUTHINFO user '.$user);
        if (PEAR::isError($response)) {
            return $response;
    	}

    	// Send the password, if the server asks
    	if (($response == 381) && ($pass !== null)) {
    	    // Send the password
            $response = $this->_sendCommand('AUTHINFO pass '.$pass);
    	    if (PEAR::isError($response)) {
    	    	return $response;
    	    }
    	}

        switch ($response) {
    	    case 281: // RFC2980: 'Authentication accepted'
    	    	if ($this->_logger) {
    	    	    $this->_logger->info("Authenticated (as user '$user')");
    	    	}

	    	// TODO: Set some variable before return

    	        return true;
    	        break;
    	    case 381: // RFC2980: 'More authentication information required'
    	        return $this->throwError('Authentication uncompleted', $response, $this->_currentStatusResponse());
    	        break;
    	    case 482: // RFC2980: 'Authentication rejected'
    	    	return $this->throwError('Authentication rejected', $response, $this->_currentStatusResponse());
    	    	break;
    	    case 502: // RFC2980: 'No permission'
    	    	return $this->throwError('Authentication rejected', $response, $this->_currentStatusResponse());
    	    	break;
//    	    case 500:
//    	    case 501:
//    	    	return $this->throwError('Authentication failed', $response, $this->_currentStatusResponse());
//    	    	break;
    	    default:
    	    	return $this->_handleUnexpectedResponse($response);
    	}
    }

    // }}}
    // {{{ cmdAuthinfoSimple()

    /**
     * Authenticate using 'simple' method
     *
     * @param string $user The username to authenticate as.
     * @param string $pass The password to authenticate with.
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdAuthinfoSimple($user, $pass)
    {
        return $this->throwError("The auth mode: 'simple' is has not been implemented yet", null);
    }

    // }}}
    // {{{ cmdAuthinfoGeneric()

    /**
     * Authenticate using 'generic' method
     *
     * @param string $user The username to authenticate as.
     * @param string $pass The password to authenticate with.
     *
     * @return mixed (bool) true on success or (object) pear_error on failure
     * @access protected
     */
    function cmdAuthinfoGeneric($user, $pass)
    {
        return $this->throwError("The auth mode: 'generic' is has not been implemented yet", null);
    }

    // }}}
    // {{{ _isConnected()

    /**
     * Test whether we are connected or not.
     *
     * @return bool true or false
     * @access protected
     */
    function _isConnected()
    {
        return (is_resource($this->_socket) && (!feof($this->_socket)));
    }

    // }}}

}

// }}}

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * c-hanging-comment-ender-p: nil
 * End:
 */

?>
