/*
 * $Id: NNTPConnection.java,v 1.6 2004/06/07 18:06:02 dog Exp $
 * Copyright (C) 2002 The Free Software Foundation
 * 
 * This file is part of GNU inetlib, a library.
 * 
 * GNU inetlib is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * GNU inetlib is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * As a special exception, if you link this library with other files to
 * produce an executable, this library does not by itself cause the
 * resulting executable to be covered by the GNU General Public License.
 * This exception does not however invalidate any other reasons why the
 * executable file might be covered by the GNU General Public License.
 */

package gnu.inet.nntp;

import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.ProtocolException;
import java.net.Socket;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import gnu.inet.util.CRLFInputStream;
import gnu.inet.util.CRLFOutputStream;
import gnu.inet.util.LineInputStream;
import gnu.inet.util.Logger;
import gnu.inet.util.MessageInputStream;

/**
 * An NNTP client.
 * This object is used to establish and manage a connection to an NNTP
 * server.
 *
 * @author <a hef='mailto:dog@gnu.org'>Chris Burdess</a>
 * @version $Revision: 1.6 $ $Date: 2004/06/07 18:06:02 $
 */
public class NNTPConnection
  implements NNTPConstants
{

  /**
   * The default NNTP port.
   */
  public static final int DEFAULT_PORT = 119;

  /**
   * The hostname of the host we are connected to.
   */
  protected String hostname;

  /**
   * The port on the host we are connected to.
   */
  protected int port;

  /**
   * The username used for authentication, if any.
   */
  protected String username;

  /**
   * The password used for authentication, if any.
   */
  protected String password;

  /**
   * The socket used for network communication.
   */
  protected Socket socket;

  /**
   * The socket input stream.
   */
  protected LineInputStream in;

  /**
   * The socket output stream.
   */
  protected CRLFOutputStream out;

  /**
   * Whether the host permits posting of articles.
   */
  protected boolean canPost;

  /**
   * The greeting issued by the host when we connected.
   */
  protected String welcome;

  /**
   * Pending data, if any.
   */
  protected PendingData pendingData;

  /**
   * Whether to log protocol-level information to stderr.
   */
  protected boolean debug;

  private static final String DOT = ".";
  private static final String US_ASCII = "US-ASCII";

  /**
   * Creates a new connection object.
   * @param hostname the hostname or IP address of the news server
   */
  public NNTPConnection (String hostname)
    throws IOException
    {
      this (hostname, DEFAULT_PORT, null, null, false);
    }

  /**
   * Creates a new connection object.
   * @param hostname the hostname or IP address of the news server
   * @param port the port to connect to
   */
  public NNTPConnection (String hostname, int port)
    throws IOException
    {
      this (hostname, port, null, null, false);
    }

  /**
   * Creates a new connection object.
   * @param hostname the hostname or IP address of the news server
   * @param username the username if authentication is required
   * @param password the password if authentication is required
   */
  public NNTPConnection (String hostname, String username, String password)
    throws IOException
    {
      this (hostname, DEFAULT_PORT, username, password, false);
    }

  /**
   * Creates a new connection object.
   * @param hostname the hostname or IP address of the news server
   * @param port the port to connect to
   * @param username the username if authentication is required
   * @param password the password if authentication is required
   */
  public NNTPConnection (String hostname, int port,
      String username, String password)
    throws IOException
    {
      this (hostname, port, username, password, false);
    }

  /**
   * Creates a new connection object.
   * @param hostname the hostname or IP address of the news server
   * @param port the port to connect to
   * @param username the username if authentication is required
   * @param password the password if authentication is required
   * @param debug whether to use debugging
   */
  public NNTPConnection (String hostname, int port,
      String username, String password, boolean debug)
    throws IOException
    {
      if (port < 0)
        {
          port = DEFAULT_PORT;
        }
      
      this.hostname = hostname;
      this.port = port;
      this.username = username;
      this.password = password;
      this.debug = debug;
      
      // Set up the socket and streams
      socket = new Socket (hostname, port);
      InputStream in = socket.getInputStream ();
      in = new CRLFInputStream (in);
      this.in = new LineInputStream (in);
      OutputStream out = socket.getOutputStream ();
      out = new BufferedOutputStream (out);
      this.out = new CRLFOutputStream (out);
      
      // Read the welcome message (RFC977:2.4.3)
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case POSTING_ALLOWED:
          canPost = true;
        case NO_POSTING_ALLOWED:
          welcome = response.getMessage ();
          break;
        default:
          throw new NNTPException (response);
        }
    }

  /**
   * Return the welcome message sent by the server in reply to the initial
   * connection.
   * This message sometimes contains disclaimers or help information that
   * may be relevant to the user.
   */
  public String getWelcome ()
    {
      return welcome;
    }

  /*
   * Returns an NNTP-format date string.
   * This is only required when clients use the NEWGROUPS or NEWNEWS
   * methods, therefore rarely: we don't cache any of the variables here.
   */
  String formatDate (Date date)
    {
      DateFormat df = new SimpleDateFormat ("yyMMdd HHmmss 'GMT'");
      Calendar cal = new GregorianCalendar ();
      TimeZone gmt = TimeZone.getTimeZone ("GMT");
      cal.setTimeZone (gmt);
      df.setCalendar (cal);
      cal.setTime (date);
      return df.format (date);
    }

  /*
   * Parse the specfied NNTP date text.
   */
  Date parseDate (String text)
    throws ParseException
    {
      DateFormat df = new SimpleDateFormat ("yyMMdd HHmmss 'GMT'");
      return df.parse (text);
    }

  // RFC977:3.1 The ARTICLE, BODY, HEAD, and STAT commands

  /**
   * Send an article retrieval request to the server.
   * @param articleNumber the article number of the article to retrieve
   * @return an article response consisting of the article number and
   * message-id, and an iterator over the lines of the article header and
   * body, separated by an empty line
   */
  public ArticleResponse article (int articleNumber)
    throws IOException
    {
      return articleImpl (ARTICLE, Integer.toString(articleNumber));
    }

  /**
   * Send an article retrieval request to the server.
   * @param messageId the message-id of the article to retrieve
   * @return an article response consisting of the article number and
   * message-id, and an iterator over the lines of the article header and
   * body, separated by an empty line
   */
  public ArticleResponse article (String messageId)
    throws IOException
    {
      return articleImpl (ARTICLE, messageId);
    }

  /**
   * Send an article head retrieval request to the server.
   * @param articleNumber the article number of the article to head
   * @return an article response consisting of the article number and
   * message-id, and an iterator over the lines of the article header
   */
  public ArticleResponse head (int articleNumber)
    throws IOException
    {
      return articleImpl (HEAD, Integer.toString (articleNumber));
    }

  /**
   * Send an article head retrieval request to the server.
   * @param messageId the message-id of the article to head
   * @return an article response consisting of the article number and
   * message-id, and an iterator over the lines of the article header
   */
  public ArticleResponse head (String messageId)
    throws IOException
    {
      return articleImpl (HEAD, messageId);
    }

  /**
   * Send an article body retrieval request to the server.
   * @param articleNumber the article number of the article to body
   * @return an article response consisting of the article number and
   * message-id, and an iterator over the lines of the article body
   */
  public ArticleResponse body (int articleNumber)
    throws IOException
    {
      return articleImpl (BODY, Integer.toString (articleNumber));
    }

  /**
   * Send an article body retrieval request to the server.
   * @param messageId the message-id of the article to body
   * @return an article response consisting of the article number and
   * message-id, and an iterator over the lines of the article body
   */
  public ArticleResponse body (String messageId)
    throws IOException
    {
      return articleImpl (BODY, messageId);
    }

  /**
   * Send an article status request to the server.
   * @param articleNumber the article number of the article to stat
   * @return an article response consisting of the article number and
   * message-id
   */
  public ArticleResponse stat (int articleNumber)
    throws IOException
    {
      return articleImpl (STAT, Integer.toString (articleNumber));
    }

  /**
   * Send an article status request to the server.
   * @param messageId the message-id of the article to stat
   * @return an article response consisting of the article number and
   * message-id
   */
  public ArticleResponse stat (String messageId)
    throws IOException
    {
      return articleImpl (STAT, messageId);
    }

  /**
   * Performs an ARTICLE, BODY, HEAD, or STAT command.
   * @param command one of the above commands
   * @param messageId the article-number or message-id in string form
   */
  protected ArticleResponse articleImpl (String command, String messageId)
    throws IOException
    {
      if (messageId != null)
        {
          StringBuffer line = new StringBuffer (command);
          line.append (' ');
          line.append (messageId);
          send (line.toString ());
        }
      else
        {
          send (command);
        }
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case ARTICLE_FOLLOWS:
        case HEAD_FOLLOWS:
        case BODY_FOLLOWS:
          ArticleResponse aresponse = (ArticleResponse) response;
          ArticleStream astream =
            new ArticleStream (new MessageInputStream (in));
          pendingData = astream;
          aresponse.in = astream;
          return aresponse;
        case ARTICLE_RETRIEVED:
          return (ArticleResponse) response;
        default:
          // NO_GROUP_SELECTED
          // NO_ARTICLE_SELECTED
          // NO_SUCH_ARTICLE_NUMBER
          // NO_SUCH_ARTICLE
          // NO_PREVIOUS_ARTICLE
          // NO_NEXT_ARTICLE
          throw new NNTPException (response);
        }
    }
  
  // RFC977:3.2 The GROUP command

  /**
   * Send a group selection command to the server.
   * Returns a group status response.
   * @param name the name of the group to select
   */
  public GroupResponse group (String name)
    throws IOException
    {
      StringBuffer line = new StringBuffer (GROUP);
      line.append (' ');
      line.append (name);
      send (line.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case GROUP_SELECTED:
          return (GroupResponse) response;
        default:
          // NO_SUCH_GROUP
          throw new NNTPException (response);
        }
    }

  // RFC977:3.3 The HELP command

  /**
   * Requests a help listing.
   * @return an iterator over a collection of help lines.
   */
  public LineIterator help ()
    throws IOException
    {
      send (HELP);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case HELP_TEXT:
          LineIterator li = new LineIterator (this);
          pendingData = li;
          return li;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC977:3.4 The IHAVE command

  /**
   * Sends an ihave command indicating that the client has an article with
   * the specified message-id.
   * @param messageId the article message-id
   * @return a PostStream if the server wants the specified article, null
   * otherwise
   */
  public PostStream ihave (String messageId)
    throws IOException
    {
      send (IHAVE);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case SEND_TRANSFER_ARTICLE:
          return new PostStream (this, false);
        case ARTICLE_NOT_WANTED:
          return null;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC(77:3.5 The LAST command

  /**
   * Sends a previous article positioning command to the server.
   * @return the article number/message-id pair associated with the new
   * article
   */
  public ArticleResponse last ()
    throws IOException
    {
      return articleImpl (LAST, null);
    }

  // RFC977:3.6 The LIST command

  /**
   * Send a group listing command to the server.
   * Returns a GroupIterator. This must be read fully before other commands
   * are issued.
   */
  public GroupIterator list ()
    throws IOException
    {
      return listImpl (LIST);
    }

  GroupIterator listImpl (String command)
    throws IOException
    {
      send (command);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case LIST_FOLLOWS:
          GroupIterator gi = new GroupIterator (this);
          pendingData = gi;
          return gi;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC977:3.7 The NEWGROUPS command

  /**
   * Returns an iterator over the list of new groups on the server since the
   * specified date.
   * NB this method suffers from a minor millenium bug.
   * 
   * @param since the date from which to list new groups
   * @param distributions if non-null, an array of distributions to match
   */
  public LineIterator newGroups (Date since, String[]distributions)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (NEWGROUPS);
      buffer.append (' ');
      buffer.append (formatDate (since));
      if (distributions != null)
        {
          buffer.append (' ');
          for (int i = 0; i < distributions.length; i++)
            {
              if (i > 0)
                buffer.append (',');
              buffer.append (distributions[i]);
            }
        }
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case NEWGROUPS_LIST_FOLLOWS:
          LineIterator li = new LineIterator (this);
          pendingData = li;
          return li;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC977:3.8 The NEWNEWS command

  /**
   * Returns an iterator over the list of message-ids posted or received to
   * the specified newsgroup(s) since the specified date.
   * NB this method suffers from a minor millenium bug.
   *
   * @param newsgroup the newsgroup wildmat
   * @param since the date from which to list new articles
   * @param distributions if non-null, a list of distributions to match
   */
  public LineIterator newNews(String newsgroup, Date since,
                              String[] distributions)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (NEWNEWS);
      buffer.append (' ');
      buffer.append (newsgroup);
      buffer.append (' ');
      buffer.append (formatDate (since));
      if (distributions != null)
        {
          buffer.append (' ');
          for (int i = 0; i < distributions.length; i++)
            {
              if (i > 0)
                buffer.append (',');
              buffer.append (distributions[i]);
            }
        }
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case NEWNEWS_LIST_FOLLOWS:
          LineIterator li = new LineIterator (this);
          pendingData = li;
          return li;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC977:3.9 The NEXT command

  /**
   * Sends a next article positioning command to the server.
   * @return the article number/message-id pair associated with the new
   * article
   */
  public ArticleResponse next ()
    throws IOException
    {
      return articleImpl (NEXT, null);
    }

  // RFC977:3.10 The POST command

  /**
   * Post an article. This is a two-stage process.
   * If successful, returns an output stream to write the article to.
   * Clients should call <code>write()</code> on the stream for all the
   * bytes of the article, and finally call <code>close()</code>
   * on the stream.
   * No other method should be called in between.
   * @see #postComplete
   */
  public OutputStream post ()
    throws IOException
    {
      send (POST);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case SEND_ARTICLE:
          return new PostStream (this, false);
        default:
          // POSTING_NOT_ALLOWED
          throw new NNTPException (response);
        }
    }

  /**
   * Indicates that the client has finished writing all the bytes of the
   * article.
   * Called by the PostStream during <code>close()</code>.
   * @see #post
   */
  void postComplete ()
    throws IOException
    {
      send (DOT);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case ARTICLE_POSTED:
        case ARTICLE_TRANSFERRED:
          return;
        default:
          // POSTING_FAILED
          // TRANSFER_FAILED
          // ARTICLE_REJECTED
          throw new NNTPException (response);
        }
    }

  // RFC977:3.11 The QUIT command

  /**
   * Close the connection.
   * After calling this method, no further calls on this object are valid.
   */
  public void quit ()
    throws IOException
    {
      send (QUIT);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case CLOSING_CONNECTION:
          socket.close ();
          return;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC977:3.12 The SLAVE command

  /**
   * Indicates to the server that this is a slave connection.
   */
  public void slave ()
    throws IOException
    {
      send (SLAVE);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case SLAVE_ACKNOWLEDGED:
          break;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC2980:1.1 The CHECK command

  public boolean check (String messageId)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (CHECK);
      buffer.append (' ');
      buffer.append (messageId);
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case SEND_ARTICLE_VIA_TAKETHIS:
          return true;
        case ARTICLE_NOT_WANTED_VIA_TAKETHIS:
          return false;
        default:
          // SERVICE_DISCONTINUED
          // TRY_AGAIN_LATER
          // TRANSFER_PERMISSION_DENIED
          // COMMAND_NOT_RECOGNIZED
          throw new NNTPException (response);
        }
    }

  // RFC2980:1.2 The MODE STREAM command

  /**
   * Attempt to initialise the connection in streaming mode.
   * This is generally used to bypass the lock step nature of NNTP in order
   * to perform a series of CHECK and TAKETHIS commands.
   *
   * @return true if the server supports streaming mode
   */
  public boolean modeStream ()
    throws IOException
    {
      send (MODE_STREAM);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case STREAMING_OK:
          return true;
        default:
          // COMMAND_NOT_RECOGNIZED
          return false;
        }
    }

  // RFC2980:1.3 The TAKETHIS command

  /**
   * Implements the out-of-order takethis command.
   * The client uses the returned output stream to write all the bytes of the
   * article. When complete, it calls <code>close()</code> on the
   * stream.
   * @see #takethisComplete
   */
  public OutputStream takethis (String messageId)
    throws IOException
    {
      send (TAKETHIS);
      return new PostStream (this, true);
    }

  /**
   * Completes a takethis transaction.
   * Called by PostStream.close().
   * @see #takethis
   */
  void takethisComplete ()
    throws IOException
    {
      send (DOT);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case ARTICLE_TRANSFERRED_OK:
          return;
        default:
          // SERVICE_DISCONTINUED
          // ARTICLE_TRANSFER_FAILED
          // TRANSFER_PERMISSION_DENIED
          // COMMAND_NOT_RECOGNIZED
          throw new NNTPException (response);
        }
    }

  // RFC2980:1.4 The XREPLIC command

  // TODO

  // RFC2980:2.1.2 The LIST ACTIVE command

  /**
   * Returns an iterator over the groups specified according to the wildmat
   * pattern. The iterator must be read fully before other commands are
   * issued.
   * @param wildmat the wildmat pattern. If null, returns all groups. If no
   * groups are matched, returns an empty iterator.
   */
  public GroupIterator listActive (String wildmat)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (LIST_ACTIVE);
      if (wildmat != null)
        {
          buffer.append (' ');
          buffer.append (wildmat);
        }
      return listImpl (buffer.toString ());
    }

  // RFC2980:2.1.3 The LIST ACTIVE.TIMES command

  /**
   * Returns an iterator over the active.times file.
   * Each ActiveTime object returned provides details of who created the
   * newsgroup and when.
   */
  public ActiveTimesIterator listActiveTimes ()
    throws IOException
    {
      send (LIST_ACTIVE_TIMES);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case LIST_FOLLOWS:
          return new ActiveTimesIterator (this);
        default:
          throw new NNTPException (response);
        }
    }

  // RFC2980:2.1.4 The LIST DISTRIBUTIONS command

  // TODO

  // RFC2980:2.1.5 The LIST DISTRIB.PATS command

  // TODO

  // RFC2980:2.1.6 The LIST NEWSGROUPS command

  /**
   * Returns an iterator over the group descriptions for the given groups.
   * @param wildmat if non-null, limits the groups in the iterator to the
   * specified pattern
   * @return an iterator over group name/description pairs
   * @see #xgtitle
   */
  public PairIterator listNewsgroups (String wildmat)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (LIST_NEWSGROUPS);
      if (wildmat != null)
        {
          buffer.append (' ');
          buffer.append (wildmat);
        }
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case LIST_FOLLOWS:
          PairIterator pi = new PairIterator (this);
          pendingData = pi;
          return pi;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC2980:2.1.7 The LIST OVERVIEW.FMT command

  /**
   * Returns an iterator over the order in which headers are stored in the
   * overview database.
   * Each line returned by the iterator contains one header field.
   * @see #xover
   */
  public LineIterator listOverviewFmt ()
    throws IOException
    {
      send (LIST_OVERVIEW_FMT);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case LIST_FOLLOWS:
          LineIterator li = new LineIterator (this);
          pendingData = li;
          return li;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC2980:2.1.8 The LIST SUBSCRIPTIONS command

  /**
   * Returns a list of newsgroups suitable for new users of the server.
   */
  public GroupIterator listSubscriptions ()
    throws IOException
    {
      return listImpl (LIST_SUBSCRIPTIONS);
    }

  // RFC2980:2.2 The LISTGROUP command

  /**
   * Returns a listing of all the article numbers in the specified
   * newsgroup. If the <code>group</code> parameter is null, the currently
   * selected group is assumed.
   * @param group the name of the group to list articles for
   */
  public ArticleNumberIterator listGroup (String group)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (LISTGROUP);
      if (group != null)
        {
          buffer.append (' ');
          buffer.append (group);
        }
      send (buffer.toString ());
      StatusResponse response = parseResponse (read (), true);
      switch (response.status)
        {
        case GROUP_SELECTED:
          ArticleNumberIterator ani = new ArticleNumberIterator (this);
          pendingData = ani;
          return ani;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC2980:2.3 The MODE READER command

  /**
   * Indicates to the server that this is a user-agent.
   * @return true if posting is allowed, false otherwise.
   */
  public boolean modeReader ()
    throws IOException
    {
      send (MODE_READER);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case POSTING_ALLOWED:
          canPost = true;
          return canPost;
        case POSTING_NOT_ALLOWED:
          canPost = false;
          return canPost;
        default:
          throw new NNTPException (response);
        }
    }
  
  // RFC2980:2.4 The XGTITLE command

  /**
   * Returns an iterator over the list of newsgroup descriptions.
   * @param wildmat if non-null, the newsgroups to match
   */
  public PairIterator xgtitle (String wildmat)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (XGTITLE);
      if (wildmat != null)
        {
          buffer.append (' ');
          buffer.append (wildmat);
        }
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case XGTITLE_LIST_FOLLOWS:
          PairIterator pi = new PairIterator (this);
          pendingData = pi;
          return pi;
        default:
          throw new NNTPException (response);
        }
    }

  // RFC2980:2.6 The XHDR command

  public HeaderIterator xhdr (String header, String range)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (XHDR);
      buffer.append (' ');
      buffer.append (header);
      if (range != null)
        {
          buffer.append (' ');
          buffer.append (range);
        }
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case HEAD_FOLLOWS:
          HeaderIterator hi = new HeaderIterator (this);
          pendingData = hi;
          return hi;
        default:
          // NO_GROUP_SELECTED
          // NO_SUCH_ARTICLE
          throw new NNTPException (response);
        }
    }

  // RFC2980:2.7 The XINDEX command

  // TODO

  // RFC2980:2.8 The XOVER command

  public OverviewIterator xover (Range range)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (XOVER);
      if (range != null)
        {
          buffer.append (' ');
          buffer.append (range.toString());
        }
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case OVERVIEW_FOLLOWS:
          OverviewIterator oi = new OverviewIterator (this);
          pendingData = oi;
          return oi;
        default:
          // NO_GROUP_SELECTED
          // PERMISSION_DENIED
          throw new NNTPException (response);
        }
    }

  // RFC2980:2.9 The XPAT command

  // TODO

  // RFC2980:2.10 The XPATH command

  // TODO

  // RFC2980:2.11 The XROVER command

  // TODO

  // RFC2980:2.12 The XTHREAD command

  // TODO

  // RFC2980:3.1.1 Original AUTHINFO

  /**
   * Basic authentication strategy.
   * @param username the user to authenticate
   * @param password the (cleartext) password
   * @return true on success, false on failure
   */
  public boolean authinfo (String username, String password)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (AUTHINFO_USER);
      buffer.append (' ');
      buffer.append (username);
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case AUTHINFO_OK:
          return true;
        case SEND_AUTHINFOPASS:
          buffer.setLength (0);
          buffer.append (AUTHINFO_PASS);
          buffer.append (' ');
          buffer.append (password);
          send (buffer.toString ());
          response = parseResponse (read ());
          switch (response.status)
            {
            case AUTHINFO_OK:
              return true;
            case PERMISSION_DENIED:
              return false;
            default:
              throw new NNTPException (response);
            }
        default:
          // AUTHINFO_REJECTED
          throw new NNTPException (response);
        }
    }

  // RFC2980:3.1.2 AUTHINFO SIMPLE

  /**
   * Implementation of NNTP simple authentication.
   * Note that use of this authentication strategy is highly deprecated,
   * only use on servers that won't accept any other form of authentication.
   */
  public boolean authinfoSimple (String username, String password)
    throws IOException
    {
      send (AUTHINFO_SIMPLE);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case SEND_AUTHINFO_SIMPLE:
          StringBuffer buffer = new StringBuffer (username);
          buffer.append (' ');
          buffer.append (password);
          send (buffer.toString ());
          response = parseResponse (read ());
          switch (response.status)
            {
            case AUTHINFO_SIMPLE_OK:
              return true;
            case AUTHINFO_SIMPLE_DENIED:
              return false;
            default:throw new NNTPException (response);
            }
        default:
          throw new NNTPException (response);
        }
    }

  // RFC2980:3.1.3 AUTHINFO GENERIC

  /**
   * Generic authentication strategy.
   * @param authenticator the name of the authentication method
   * @param args authenticator parameters in space-separated form...
   */
  public boolean authinfoGeneric (String authenticator, String args)
    throws IOException
    {
      StringBuffer buffer = new StringBuffer (AUTHINFO_GENERIC);
      buffer.append (' ');
      buffer.append (authenticator);
      buffer.append (' ');
      buffer.append (args);
      send (buffer.toString ());
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case COMMAND_NOT_RECOGNIZED:
        case SYNTAX_ERROR:
        case INTERNAL_ERROR:
          throw new NNTPException (response);
        default:
          // TODO attempt to connect authenticator
          // ... do stuff ...
          // PERMISSION_DENIED -> false
          // AUTHINFO_OK -> true
        }
      return false;
    }
  
  // RFC2980:3.2 The DATE command

  /**
   * Returns the date on the server.
   */
  public Date date ()
    throws IOException
    {
      send (DATE);
      StatusResponse response = parseResponse (read ());
      switch (response.status)
        {
        case DATE_OK:
          String message = response.getMessage ();
          try
            {
              DateFormat df = new SimpleDateFormat ("yyyyMMddHHmmss");
              return df.parse (message);
            }
          catch (ParseException e)
            {
              throw new IOException ("Invalid date: " + message);
            }
        default:
          throw new NNTPException (response);
        }
    }

  // -- Utility functions --

  /**
   * Parse a response object from a response line sent by the server.
   */
  protected StatusResponse parseResponse (String line)
    throws ProtocolException
    {
      return parseResponse (line, false);
    }
  
  /**
   * Parse a response object from a response line sent by the server.
   * @param isListGroup whether we are invoking the LISTGROUP command
   */
  protected StatusResponse parseResponse (String line, boolean isListGroup)
    throws ProtocolException
    {
      int start = 0, end;
      short status = -1;
      String statusText = line;
      String message = null;
      end = line.indexOf (' ', start);
      if (end > start)
        {
          statusText = line.substring (start, end);
          message = line.substring (end + 1);
        }
      try
        {
          status = Short.parseShort (statusText);
        }
      catch (NumberFormatException e)
        {
          throw new ProtocolException (line);
        }
      StatusResponse response;
      switch (status)
        {
        case ARTICLE_FOLLOWS:
        case HEAD_FOLLOWS:
        case BODY_FOLLOWS:
        case ARTICLE_RETRIEVED:
        case GROUP_SELECTED:
          /* The LISTGROUP command returns a list of articles with a 211,
           * instead of the newsgroup totals returned with the GROUP command.
           * Check for this case. */
          if (status != GROUP_SELECTED || isListGroup)
            {
              try
                {
                  ArticleResponse aresponse =
                    new ArticleResponse (status, message);
                  // article number
                  start = end + 1;
                  end = line.indexOf (' ', start);
                  if (end > start)
                    {
                      aresponse.articleNumber =
                        Integer.parseInt (line.substring (start, end));
                    }
                  // message-id
                  start = end + 1;
                  end = line.indexOf (' ', start);
                  if (end > start)
                    {
                      aresponse.messageId = line.substring (start, end);
                    }
                  else
                    {
                      aresponse.messageId = line.substring (start);
                    }
                  response = aresponse;
                }
              catch (NumberFormatException e)
                {
                  // This will happen for XHDR
                  response = new StatusResponse (status, message);
                }
              break;
            }
          // This is the normal case for GROUP_SELECTED
          GroupResponse gresponse = new GroupResponse (status, message);
          try
            {
              // count
              start = end + 1;
              end = line.indexOf (' ', start);
              if (end > start)
                {
                  gresponse.count =
                    Integer.parseInt (line.substring (start, end));
                }
              // first
              start = end + 1;
              end = line.indexOf (' ', start);
              if (end > start)
                {
                  gresponse.first =
                    Integer.parseInt (line.substring (start, end));
                }
              // last
              start = end + 1;
              end = line.indexOf (' ', start);
              if (end > start)
                {
                  gresponse.last =
                    Integer.parseInt (line.substring (start, end));
                }
              // group
              start = end + 1;
              end = line.indexOf (' ', start);
              if (end > start)
                {
                  gresponse.group = line.substring (start, end);
                }
              else
                {
                  gresponse.group = line.substring (start);
                }
            }
          catch (NumberFormatException e)
            {
              throw new ProtocolException (line);
            }
          response = gresponse;
          break;
        default:
          response = new StatusResponse (status, message);
        }
      return response;
    }
  
  /**
   * Send a single line to the server.
   * @param line the line to send
   */
  protected void send(String line)
    throws IOException
    {
      if (pendingData != null)
        {
          // Clear pending data
          pendingData.readToEOF ();
          pendingData = null;
        }
      if (debug)
        {
          Logger logger = Logger.getInstance ();
          logger.log ("nntp", ">" + line);
        }
      byte[] data = line.getBytes (US_ASCII);
      out.write (data);
      out.writeln ();
      out.flush ();
    }
  
  /**
   * Read a single line from the server.
   * @return a line of text
   */
  protected String read ()
    throws IOException
    {
      String line = in.readLine ();
      if (debug)
        {
          Logger logger = Logger.getInstance ();
          if (line == null)
            logger.log ("nntp", "<EOF");
          else
            logger.log ("nntp", "<" + line);
        }
      return line;
    }

}
