/*
 * @(#)Nntp.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.nntp;

import com.jscape.inet.util.TimedSocket;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;


/**
 * Implements the basic functionality of a NNTP client.
 * <P>
 * Example Usage:
 * <P>
 * <pre>
 * Nntp nntp = new Nntp();
 * nntp.setHostname("news.myserver.com");
 * nntp.setUsername("jsmith");
 * nntp.setPassword("secret");
 *  try
 * {
 *   nntp.connect();
 *   nntp.setNewsgroup("comp.lang.java.programmer");
 *   // get all headers and print out subject
 *   Enumeration headers = nntp.getArticleHeaders();
 *   while(headers.hasMoreElements())
 *   {
 *     NntpArticleHeader header = (NntpArticleHeader)headers.nextElement();
 *     System.out.println(header.getSubject());
 *   }
 *   // disconnect
 *   nntp.disconnect();
 * }
 * catch(Exception e)
 * {
 *   System.out.println(e);
 * }
 * </pre>
 * </p>
 */
public class Nntp
        implements Serializable {
    /**
     * Command to send username.
     */
    public static final int CMD_AUTHUSER = 0;

    /**
     * Command to send password.
     */
    public static final int CMD_AUTHPASS = 1;

    /**
     * Command to list newsgroups.
     */
    public static final int CMD_LIST = 2;

    /**
     * Command to get new newsgroups.
     */
    public static final int CMD_NEWGROUPS = 3;

    /**
     * Command to get new articles.
     */
    public static final int CMD_NEWNEWS = 4;

    /**
     * Command to select a newsgroup.
     */
    public static final int CMD_GROUP = 5;

    /**
     * Command to get header of article.
     */
    public static final int CMD_HEAD = 6;

    /**
     * Command to get body of article.
     */
    public static final int CMD_BODY = 7;

    /**
     * Command to get article.
     */
    public static final int CMD_ARTICLE = 8;

    /**
     * Command to get information about an article.
     */
    public static final int CMD_STAT = 9;

    /**
     * Command to get next article.
     */
    public static final int CMD_NEXT = 10;

    /**
     * Command to get previous article.
     */
    public static final int CMD_LAST = 11;

    /**
     * Command to post article.
     */
    public static final int CMD_POST = 12;

    /**
     * Command to logout.
     */
    public static final int CMD_QUIT = 13;

    /**
     * Command to get NNTP help.
     */
    public static final int CMD_HELP = 14;

    /**
     * Command to indicate to NNTP server that client has an article.
     */
    public static final int SCMD_IHAVE = 15;

    /**
     * Command to indicate to NNTP server that this client connection is to a
     * slave server rather than a user.
     */
    public static final int SCMD_SLAVE = 16;

    /**
     * Command to indicate to NNTP server that this is a NNTP reader.
     */
    public static final int MODE = 17;

    /**
     * Command to get a set of articles from NNTP server.
     */
    public static final int XOVER = 18;

    /**
     * Represents a carriage return.
     */
    private static final byte CR = 13;

    /**
     * Represents a line feed.
     */
    private static final byte LF = 10;

    /**
     * Represents a period.
     */
    private static final byte PERIOD = 46;

    /**
     * List of available NNTP commands as Strings.
     */
    private static final String[] COMMANDS = {
            "AUTHINFO USER", "AUTHINFO PASS", "LIST", "NEWGROUPS", "NEWNEWS",
            "GROUP", "HEAD", "BODY", "ARTICLE", "STAT", "NEXT", "LAST", "POST",
            "QUIT", "HELP", "IHAVE", "SLAVE", "MODE", "XOVER"
    };

    /**
     * Represents carriage return new line combination.
     */
    private static final String NEWLINE = new String("\r\n");

    /**
     * Response for help.
     */
    public static final int RESPONSE_HELP = 100;

    /**
     * Response for help.
     */
    public static final int RESPONSE_DEBUG = 199;

    /**
     * Response to indicate posting to NNTP server allowed.
     */
    public static final int RESPONSE_OK_POSTING_ALLOWED = 200;

    /**
     * Response to indicate posting to NNTP server not allowed.
     */
    public static final int RESPONSE_OK_POSTING_NOT_ALLOWED = 201;

    /**
     * Response to indicate that slave status is noted.
     */
    public static final int RESPONSE_SLAVE_NOTED = 202;

    /**
     * Response code closing connection.
     */
    public static final int RESPONSE_CLOSING_CONNECTION = 205;

    /**
     * Response code group selected.
     */
    public static final int RESPONSE_GROUP_SELECTED = 211;

    /**
     * Response code article retrieved head and body follows.
     */
    public static final int RESPONSE_ARTICLE_RETRIEVED_HEAD_AND_BODY_FOLLOW = 220;

    /**
     * Response code article retrieved head follows.
     */
    public static final int RESPONSE_ARTICLE_RETRIEVED_HEAD_FOLLOWS = 221;

    /**
     * Response code article retrieved body follows.
     */
    public static final int RESPONSE_ARTICLE_RETRIEVED_BODY_FOLLOWS = 222;

    /**
     * Response code article retrieved request text separately.
     */
    public static final int RESPONSE_ARTICLE_RETRIEVED_REQUEST_TEXT_SEPARATELY = 223;

    /**
     * Response code article list by message-id follows.
     */
    public static final int RESPONSE_ARTICLE_LIST_BY_MESSAGE_ID_FOLLOWS = 230;

    /**
     * Response code newsgroup list follows.
     */
    public static final int RESPONSE_NEW_NEWSGROUP_LIST_FOLLOWS = 231;

    /**
     * Response code article transferred.
     */
    public static final int RESPONSE_ARTICLE_TRANSFERRED_OK = 235;

    /**
     * Response code article posted.
     */
    public static final int RESPONSE_ARTICLE_POSTED_OK = 240;

    /**
     * Response code authentication passed.
     */
    public static final int RESPONSE_AUTH_OK = 250;

    /**
     * Response code authentication passed.
     */
    public static final int RESPONSE_AUTH_OK_OLD = 281;

    /**
     * Response code send article to transfer.
     */
    public static final int RESPONSE_SEND_ARTICLE_TO_TRANSFER = 335;

    /**
     * Response code send article to post.
     */
    public static final int RESPONSE_SEND_ARTICLE_TO_POST = 340;

    /**
     * Response code continue authentication.
     */
    public static final int RESPONSE_CONTINUE_AUTH = 350;

    /**
     * Response code authentication failed.
     */
    public static final int RESPONSE_AUTH_FAILED_OLD = 381;

    /**
     * Response code service temporarily unavailable.
     */
    public static final int RESPONSE_SERVICE_DISCONTINUED = 400;

    /**
     * Response code no such newsgroup.
     */
    public static final int RESPONSE_NO_SUCH_NEWSGROUP = 411;

    /**
     * Response code no newsgroup selected.
     */
    public static final int RESPONSE_NO_NEWSGROUP_SELECTED = 412;

    /**
     * Response code no current article selected.
     */
    public static final int RESPONSE_NO_CURRENT_ARTICLE_SELECTED = 420;

    /**
     * Response code no next article.
     */
    public static final int RESPONSE_NO_NEXT_ARTICLE = 421;

    /**
     * Response code no previous article.
     */
    public static final int RESPONSE_NO_PREVIOUS_ARTICLE = 422;

    /**
     * Response code no such article number.
     */
    public static final int RESPONSE_NO_SUCH_ARTICLE_NUMBER = 423;

    /**
     * Response code article not found.
     */
    public static final int RESPONSE_NO_SUCH_ARTICLE_FOUND = 430;

    /**
     * Response code article not wanted.
     */
    public static final int RESPONSE_ARTICLE_NOT_WANTED = 435;

    /**
     * Response code transfer failed.
     */
    public static final int RESPONSE_TRANSFER_FAILED = 436;

    /**
     * Response code article rejected.
     */
    public static final int RESPONSE_ARTICLE_REJECTED = 437;

    /**
     * Response code posting not allowed.
     */
    public static final int RESPONSE_POSTING_NOT_ALLOWED = 440;

    /**
     * Response code posting failed.
     */
    public static final int RESPONSE_POSTING_FAILED = 441;

    /**
     * Response code authentication failed.
     */
    public static final int RESPONSE_AUTH_FAILED = 450;

    /**
     * Response code post rejected.
     */
    public static final int RESPONSE_POSTING_REJECTED = 452;

    /**
     * Response code authentication failed.
     */
    public static final int RESPONSE_AUTH_FAILED_OLD2 = 480;

    /**
     * Response code post rejected.
     */
    public static final int RESPONSE_POSTING_REJECTED_OLD = 482;

    /**
     * Response code command not recognized.
     */
    public static final int RESPONSE_COMMAND_NOT_RECOGNIZED = 500;

    /**
     * Response code syntax error.
     */
    public static final int RESPONSE_COMMAND_SYNTAX_ERROR = 501;

    /**
     * Response code permission denied.
     */
    public static final int RESPONSE_PERMISSION_DENIED = 502;

    /**
     * Response code server error.
     */
    public static final int RESPONSE_PROGRAM_FAULT = 503;
    private boolean iterationStarted = false;
    private boolean debug = false;
    protected BufferedReader reader;
    protected BufferedOutputStream writer;
    protected int port = 119;
    protected int timeout = 30000;
    private NntpNewsgroup ngcurr;
    private Socket socket;
    protected String hostname;
    protected String username;
    protected String password;
    private String currentGroup;
    private transient Vector listeners = new Vector();
    protected Vector serverResponse = new Vector();

    /**
     * Holds value of property debugStream.
     */
    private transient PrintStream debugStream = System.out;

    private static final int DEFAULT_PROXY_PORT = 1080;
    private static final String DEFAULT_PROXY_TYPE = "SOCKS5";
    private String proxyHostname = null;
    private int proxyPort = DEFAULT_PROXY_PORT;
    private String proxyUsername = null;
    private String proxyPassword = null;
    private String proxyType = DEFAULT_PROXY_TYPE;

    /**
     * Constructs new <code>Nntp</code> instance.
     */
    public Nntp() {
        
    }

    /**
     * Constructs <code>Nntp</code> instance with specified server name, port
     * and username / password for authentication.
     *
     * @param hostname the hostname of NNTP server
     * @param username the username of NNTP user
     * @param password the password of NNTP user
     */
    public Nntp(String hostname, String username, String password) {
        this();
        this.hostname = hostname;
        this.username = username;
        this.password = password;
    }

    /**
     * Constructs <code>Nntp</code> instance with specified server name.
     *
     * @param hostname the hostname of NNTP server
     */
    public Nntp(String hostname) {
        this();
        this.hostname = hostname;
    }

    /**
     * Gets reponse from last NNTP command.
     *
     * @return response
     */
    public String getResponseString() {
        synchronized (this) {
            if (serverResponse.size() > 0) {
                return (String) serverResponse.lastElement();
            } else {
                return null;
            }
        }
    }

    /**
     * Gets history of all NNTP responses for this session.
     *
     * @return a Enumeration of responses
     */
    public Enumeration getResponseStrings() {
        return serverResponse.elements();
    }

    /**
     * Connects to NNTP server and authenticates using username and password if found.
     *
     * @throws NntpException thrown for I/O or NNTP related errors
     */
    public void connect()
            throws NntpException {
        synchronized (this) {
            try {
                if (this.proxyHostname == null) {
                    socket = TimedSocket.getSocket(hostname, port, timeout);
                } else {
                    socket = TimedSocket.getSocket(hostname, port, timeout,
                                                   this.proxyHostname, this.proxyPort, this.proxyUsername,
                                                   this.proxyPassword, this.proxyType);
                }
                reader = new BufferedReader(new InputStreamReader(
                        socket.getInputStream()));
                writer = new BufferedOutputStream(socket.getOutputStream());

                String response = reader.readLine();
                debug(response);

                if (code(response) >= 400) {
                    // connection success but the server
                    // may have chosen not to allow us access
                    throw new NntpException(message(response));
                }

                // volunteer authentication, this may help in
                // obtaining valid posting permissions
                if (username != null) {
                    authenticate(username, password);
                }

                try {
                    // send(MODE, "READER");
                    issueCommand(COMMANDS[MODE] + " READER");
                } catch (Exception ez) {
                    ez.printStackTrace();
                }

                this.fireNntpConnected();
            } catch (UnknownHostException e) {
                throw new NntpException("unknown host", e);
            } catch (IOException e) {
                throw new NntpException("an I/O error occurred", e);
            }
        }
    }

    /**
     * Disconnects from NNTP server.
     */
    public void disconnect() {
        synchronized (this) {
            try {
                reader.close();
            } catch (Exception e) {
                // ignore
            }

            try {
                writer.close();
            } catch (Exception e) {
                // todo: ignore?
            }

            try {
                socket.close();
            } catch (Exception e) {
                // todo: ignore?
            }

            serverResponse.removeAllElements();
            this.fireNntpDisconnected();
        }
    }

    /**
     * Sets hostname of NNTP server.
     *
     * @param hostname the hostname of NNTP server
     */
    public void setHostname(String hostname) {
        synchronized (this) {
            this.hostname = hostname;
        }
    }

    /**
     * Gets hostname of NNTP server.
     *
     * @return hostname
     */
    public String getHostname() {
        return hostname;
    }

    /**
     * Sets port of NNTP server. Default port value is 119.
     *
     * @param port the port of NNTP server
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * Gets port of NNTP server. Default port value is 119.
     *
     * @return port
     */
    public int getPort() {
        return port;
    }

    /**
     * Gets all newsgroups from NNTP server.
     *
     * @return an Enumeration of <code>NntpNewsgroup</code>
     *
     * @throws NntpException thrown for I/O or NNTP related errors
     */
    public Enumeration getNewsgroups()
            throws NntpException {
        synchronized (this) {
            try {
                // String response = send(CMD_LIST);
                String response = issueCommand(COMMANDS[CMD_LIST]);

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                Vector groups = new Vector();

                response = reader.readLine();

                while (!response.equals(".")) {
                    NntpNewsgroup a = new NntpNewsgroup(response);

                    this.fireNntpProgress(groups.size());
                    groups.addElement(a);
                    response = reader.readLine();
                }

                return groups.elements();
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets new newgroups from NNTP server since a specified date.
     *
     * @param date the start <code>Date</code> used to find new newsgroups
     *
     * @return an Enumeration of <code>NntpNewsgroup</code>
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public Enumeration getNewsgroups(Date date)
            throws NntpException {
        synchronized (this) {
            try {
                //String response = send(CMD_NEWGROUPS, getNntpDate(date));
                String response = issueCommand(COMMANDS[CMD_NEWGROUPS] + " " + getNntpDate(date));

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                Vector groups = new Vector();

                response = reader.readLine();

                while (!response.equals(".")) {
                    NntpNewsgroup a = new NntpNewsgroup(response);

                    this.fireNntpProgress(groups.size());
                    groups.addElement(a);
                    response = reader.readLine();
                }

                return groups.elements();
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets the currently active newsgroup.
     *
     * @return a <code>NntpNewsgroup</code>
     */
    public NntpNewsgroup getCurrentNewsgroup() {
        return ngcurr;
    }

    /**
     * Creates <code>NntpNewsgroup</code> from server response to GROUP command.<br>
     * Posting permission is not available in response to this command and it
     * will be assigned to true. Note that this command will not assign
     * instance current newsgroup.
     *
     * @param group the newsgroup to retrieve
     *
     * @return a <CODE>NntpNewsgroup</CODE>
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpNewsgroup getNewsgroup(String group)
            throws NntpException {
        synchronized (this) {
            try {
                // String response = send(CMD_GROUP, group);
                String response = issueCommand(COMMANDS[CMD_GROUP] + " " + group);

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                // parse group info
                NntpNewsgroup gr = new NntpNewsgroup(message(response), true);

                return gr;
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Sets current active newsgroup.
     *
     * @param group the name of newsgroup
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public void setNewsgroup(NntpNewsgroup group)
            throws NntpException {
        synchronized (this) {
            setNewsgroup(group.getName());
        }
    }

    /**
     * Sets current active newsgroup.
     *
     * @param group the name of newsgroup
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public void setNewsgroup(String group)
            throws NntpException {
        synchronized (this) {
            currentGroup = new String(group);
            ngcurr = getNewsgroup(group);
            iterationStarted = false;
        }
    }

    /**
     * Gets next article header from newsgroup.
     *
     * @return a <code>NntpArticleHeader</code> or <code>null</code> if not
     *         available
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpArticleHeader getNextHeader()
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                if (iterationStarted) {
                    //String response = send(CMD_NEXT);
                    String response = issueCommand(COMMANDS[CMD_NEXT]);

                    // no more messages
                    if (code(response) >= 400) {
                        return null;
                    }
                }

                iterationStarted = true;

                //String response = send(CMD_HEAD);
                String response = issueCommand(COMMANDS[CMD_HEAD]);
                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }
                NntpArticleHeader head = new NntpArticleHeader(readMessage());

                // message() will remove nntp code, and code will return first next integer
                // which is message id
                head.setID(code(message(response)));
                this.fireNntpArticleHeaderRetrieved(head);

                return head;
            } catch (NntpException e) {
                throw e;
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets all article headers from NNTP server within a specific range using the XOVER command.
     *
     * @param start the start article
     * @param end   the end article
     *
     * @return an Enumeration of NntpArticleHeader
     *
     * @throws NntpException if an error occurs
     */
    public Enumeration getArticleHeaders(int start, int end)
            throws NntpException {
        Vector headers = new Vector();

        try {
            String line = null;

            // get overview
            //send(XOVER, start + "-" + end);
            issueCommand(COMMANDS[XOVER] + " " + start + "-" + end);

            while ((line = reader.readLine()) != null) {
                if (line.equals(".")) {
                    break;
                } else {
                    StringTokenizer st = new StringTokenizer(line, "\t");
                    int articleId = Integer.parseInt(st.nextToken());
                    String subject = st.nextToken();
                    String from = st.nextToken();
                    String date = st.nextToken();
                    NntpArticleHeader header = new NntpArticleHeader();
                    header.setSubject(subject);
                    header.setFrom(from);
                    header.setDate(date);
                    header.setID(articleId);
                    headers.add(header);
                }
            }
        } catch (IOException ioe) {
            throw new NntpException(ioe.getMessage(), ioe);
        } catch (Exception e) {
            e.printStackTrace();
            throw new NntpException(e.getMessage(), e);
        }

        return headers.elements();
    }

    /**
     * Gets all article headers from current newsgroup.
     *
     * @return an <code>Enumeration</code> of <code>NntpArticleHeader</code>
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public Enumeration getArticleHeaders()
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            NntpNewsgroup group = getCurrentNewsgroup();

            return getArticleHeaders(group.getFirst(), group.getLast());
        }
    }

    /**
     * Gets all article headers for a newsgroup. This command will set current
     * active newsgroup to value of parameter group.
     *
     * @param group the name of newsgroup
     *
     * @return an <code>Enumeration</code> of <code>NntpArticleHeader</code>
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public Enumeration getArticleHeaders(String group)
            throws NntpException {
        synchronized (this) {
            try {
                setNewsgroup(group);

                return getArticleHeaders();
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets article header from current newsgroup.
     *
     * @param articleId the article id to retrieve
     *
     * @return a <code>NntpArticleHeader</code> or <code>null</code> if not
     *         available
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpArticleHeader getArticleHeader(int articleId)
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                // String response = send(CMD_STAT, Integer.toString(articleId));
                String response = issueCommand(COMMANDS[CMD_STAT] + " " + Integer.toString(articleId));

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                //response = send(CMD_HEAD);
                response = issueCommand(COMMANDS[CMD_HEAD]);

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                NntpArticleHeader head = new NntpArticleHeader(readMessage());

                // message() will remove nntp code, and code will return first next integer
                // which is message id
                head.setID(code(message(response)));
                this.fireNntpArticleHeaderRetrieved(head);

                return head;
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Reads message returned by NNTP server.
     *
     * @return the message as a byte[]
     *
     * @throws IOException if an I/O related error occurs
     */
    private byte[] readMessage()
            throws IOException {
        byte[] delim = { CR, LF, PERIOD, CR, LF };
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        int count = 0;

        while (true) {
            int b = reader.read();

            if (b == -1) {
                break;
            }

            bout.write(b);

            if (delim[count] == b) {
                count++;
            } else {
                // reset counter
                if (count != 0) {
                    count = 0;
                }

                // check again for match
                if (delim[count] == b) {
                    count++;
                }
            }

            if (count == delim.length) {
                break;
            }
        }

        byte[] data = bout.toByteArray();
        byte[] messageData = new byte[data.length - 3];
        System.arraycopy(data, 0, messageData, 0, data.length - 3);
        bout.close();

        return messageData;
    }

    /**
     * Gets all article headers for current active newsgroup since a specified
     * date. This method will fire a NntpProgressEvent(s) as it is usual that
     * queries like this may take a while. Once server has collected articles
     * that match query, each header is retrieved and
     * <code>NntpArticleHeaderEvent</code> is generated for each new header.
     *
     * @param date the start <code>Date</code> used to find new article headers
     *
     * @return an <code>Enumeration</code> of <code>NntpArticleHeader</code>
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public Enumeration getArticleHeaders(Date date)
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                //String response = send(CMD_NEWNEWS, currentGroup + " " + getNntpDate(date));
                String response = issueCommand(COMMANDS[CMD_NEWNEWS] + " " + currentGroup + " " + getNntpDate(date));

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                String[] newIDs = readIDList();
                Vector newHeads = new Vector();

                for (int i = 0; i < newIDs.length; i++) {
                    // response = send(CMD_STAT, newIDs[i]);
                    response = issueCommand(COMMANDS[CMD_STAT] + " " + newIDs[i]);

                    if (code(response) >= 400) {
                        throw new NntpException(message(response));
                    }

                    //response = send(CMD_HEAD);
                    response = issueCommand(COMMANDS[CMD_HEAD]);

                    if (code(response) >= 400) {
                        throw new NntpException(message(response));
                    }

                    NntpArticleHeader head = new NntpArticleHeader(readMessage());

                    // message() will remove nntp code, and code will return first next integer
                    // which is message id
                    head.setID(code(message(response)));
                    this.fireNntpArticleHeaderRetrieved(head);
                    newHeads.addElement(head);
                }

                return newHeads.elements();
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets article headers since a certain date in specified newsgroups. This
     * command will not affect current newsgroup field of this Nntp instance.
     *
     * @param groups the groups to retrieve
     * @param date   the start date to retrieve articles
     *
     * @return an <code>Enumeration</code> of <code>NntpArticleHeader</code>
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public Enumeration getArticleHeaders(String[] groups, Date date)
            throws NntpException {
        synchronized (this) {
            try {
                StringBuffer gbuf = new StringBuffer();

                for (int i = 0; i < groups.length; i++) {
                    gbuf.append(groups[i]);

                    if (i < (groups.length - 1)) {
                        gbuf.append(",");
                    }
                }

                // String response = send(CMD_NEWNEWS, gbuf.toString() + " " + getNntpDate(date));
                String response = issueCommand(COMMANDS[CMD_NEWNEWS] + " " + gbuf.toString() + " " + getNntpDate(date));

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                String[] newIDs = readIDList();
                Vector newHeads = new Vector();

                for (int i = 0; i < newIDs.length; i++) {
                    //response = send(CMD_HEAD, newIDs[i]);
                    response = issueCommand(COMMANDS[CMD_HEAD] + " " + newIDs[i]);

                    if (code(response) >= 400) {
                        throw new NntpException(message(response));
                    }

                    NntpArticleHeader head = new NntpArticleHeader(readMessage());

                    // message() will remove nntp code, and code will return first next integer
                    // which is integer article id
                    head.setID(code(message(response)));
                    this.fireNntpArticleHeaderRetrieved(head);
                    newHeads.addElement(head);
                }

                return newHeads.elements();
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets an article from current newsgroup.
     *
     * @param articleId the index based id of article
     *
     * @return a <code>NntpArticle</code> or null if not available
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpArticle getArticle(int articleId)
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                //String response = send(CMD_ARTICLE, Integer.toString(articleId));
                String response = issueCommand(COMMANDS[CMD_ARTICLE] + " " + Integer.toString(articleId));

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                NntpArticle a = new NntpArticle(readMessage());
                this.fireNntpArticleRetrieved(a);

                return a;
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets an article from current newsgroup based on its Message-ID header.
     * The messageID parameter can be obtained from 'Message-ID' header in
     * <code>NntpArticleHeader</code>.
     *
     * @param messageID the unique message id for article
     *
     * @return a <code>NntpArticle</code> or null if not found
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpArticle getArticle(String messageID)
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                // String response = send(CMD_ARTICLE, messageID);
                String response = issueCommand(COMMANDS[CMD_ARTICLE] + " " + messageID);

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                NntpArticle a = new NntpArticle(readMessage());

                this.fireNntpArticleRetrieved(a);

                return a;
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets next article from current newsgroup.
     *
     * @return a <code>NntpArticle</code> or <code>null</code> if not found
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpArticle getNextArticle()
            throws NntpException {
        NntpArticle article = null;

        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                if (iterationStarted) {
                    //String response = send(CMD_NEXT);
                    String response = issueCommand(COMMANDS[CMD_NEXT]);

                    if (code(response) >= 400) {
                        return null;
                    }
                }

                iterationStarted = true;

                //  send(CMD_ARTICLE);
                issueCommand(COMMANDS[CMD_ARTICLE]);
                article = new NntpArticle(readMessage());
                this.fireNntpArticleRetrieved(article);
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }

        return article;
    }

    /**
     * Gets previous article from current newsgroup.
     *
     * @return a <code>NntpArticle</code> or <code>null</code> if not found
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpArticle getPreviousArticle()
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                if (iterationStarted) {
                    // String response = send(CMD_LAST);
                    String response = issueCommand(COMMANDS[CMD_LAST]);

                    if (code(response) >= 400) {
                        return null;
                    }
                }

                iterationStarted = true;

                // send(CMD_ARTICLE);
                issueCommand(COMMANDS[CMD_ARTICLE]);

                NntpArticle a = new NntpArticle(readMessage());

                this.fireNntpArticleRetrieved(a);

                return a;
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Gets previous article header from current newsgroup.
     *
     * @return a <code>NntpArticleHeader</code> or <code>null</code> if not
     *         found
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public NntpArticleHeader getPreviousHeader()
            throws NntpException {
        if (ngcurr == null) {
            throw new NntpException("newsgroup not selected");
        }

        synchronized (this) {
            try {
                if (iterationStarted) {
                    // String response = send(CMD_LAST);
                    String response = issueCommand(COMMANDS[CMD_LAST]);

                    if (code(response) >= 400) {
                        return null;
                    }
                }

                iterationStarted = true;

                //String response = send(CMD_HEAD);
                String response = issueCommand(COMMANDS[CMD_HEAD]);
                NntpArticleHeader head = new NntpArticleHeader(readMessage());

                // message() will remove nntp code, and code will return first next integer
                // which is message id
                head.setID(code(message(response)));
                this.fireNntpArticleHeaderRetrieved(head);

                return head;
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Authenticates client with username/password supplied by
     * <code>setUsername</code> and <code>setPassword</code> or
     * <code>Nntp</code> constructor.
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    protected void authenticate()
            throws NntpException {
        authenticate(username, password);
    }

    /**
     * Authenticates client with username/password supplied. This method will
     * set instance fields username and password.
     *
     * @param username the username of Nntp user
     * @param password the password of Nntp user
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    protected void authenticate(String username, String password)
            throws NntpException {
        synchronized (this) {
            setUsername(username);
            setPassword(password);

            try {
                //String response = send(CMD_AUTHUSER, username);
                String response = issueCommand(COMMANDS[CMD_AUTHUSER] + " " + username);

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                if (code(response) > 300) {
                    //response = send(CMD_AUTHPASS, password);
                    response = issueCommand(COMMANDS[CMD_AUTHPASS] + " " + password);

                    if (code(response) >= 400) {
                        throw new NntpException(message(response));
                    }
                }
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Posts an article to NNTP server. If authentication is required and
     * username is supplied, this method will try to authenticate. Password
     * may be sent only if required by server per RFC 2980.
     *
     * @param article the <code>NntpArticle</code> to post
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    public void postArticle(NntpArticle article)
            throws NntpException {
        synchronized (this) {
            try {
                // String response = send(CMD_POST);
                String response = issueCommand(COMMANDS[CMD_POST]);
                int code = code(response);

                if (code >= 500) {
                    throw new NntpException(message(response));
                }

                if ((code == 350) || (code == 381) || (code == 450) || (code == 480)) {
                    authenticate();
                    //response = send(CMD_POST);
                    response = issueCommand(COMMANDS[CMD_POST]);

                    if (code(response) >= 400) {
                        throw new NntpException(message(response));
                    }
                }

                writer.write(article.getMessage());
                writer.write("\r\n.\r\n".getBytes());
                writer.flush();
                response = reader.readLine();
                debug(response);

                if (code(response) >= 400) {
                    throw new NntpException(message(response));
                }

                this.fireNntpArticlePosted(article);
            } catch (IOException e) {
                disconnect();
                throw new NntpException("an I/O error occurred", e);
            } catch (Exception e) {
                throw new NntpException(e.getMessage());
            }
        }
    }

    /**
     * Sets username for authentication.
     *
     * @param username the username of Nntp user
     */
    public void setUsername(String username) {
        synchronized (this) {
            this.username = username;
        }
    }

    /**
     * Gets username currently used for authentication.
     *
     * @return username
     */
    public String getUsername() {
        return username;
    }

    /**
     * Sets password for authentication.
     *
     * @param password the password of NNTP user
     */
    public void setPassword(String password) {
        synchronized (this) {
            this.password = password;
        }
    }

    /**
     * Sets the hostname of the proxy server to be used in Nntp communications.
     *
     * @param proxyHost the hostname or ipaddress of the proxy server
     */
    public void setProxyHost(String proxyHost) {
        this.proxyHostname = proxyHost;
    }

    /**
     * Gets the hostname of the proxy server to be used in Nntp communications.
     *
     * @return the hostname or ipaddress of the proxy server
     */
    public String getProxyHost() {
        return proxyHostname;
    }

    /**
     * Sets the port to be used when communicating with an Nntp proxy server.
     *
     * @param proxyPort port of listening Nntp proxy server
     */
    public void setProxyPort(int proxyPort) {
        this.proxyPort = proxyPort;
    }

    /**
     * Gets the port to be used when communicating with an Nntp proxy server.
     *
     * @return the port of listening Nntp proxy server
     */
    public int getProxyPort() {
        return proxyPort;
    }

    /**
     * Sets the username and password to use when for authentication with
     * proxy server.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyUsername the proxy username
     * @param proxyPassword the proxy password
     *
     * @see #clearProxySettings
     */
    public void setProxyAuthentication(String proxyUsername,
                                       String proxyPassword) {
        this.proxyUsername = proxyUsername;
        this.proxyPassword = proxyPassword;
    }

    // PROXY CONTROL

    /**
     * Sets the proxy hostname and port for this connection.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyHostname the hostname or ip address of the proxy server
     * @param proxyPort     the port of the proxy server
     *
     * @see #clearProxySettings
     */
    public void setProxyHost(String proxyHostname, int proxyPort) {
        this.proxyHostname = proxyHostname;
        this.proxyPort = proxyPort;
    }

    /**
     * Sets the proxy type will be used for this connection.
     *
     * @param proxyType The proxy type. Valid values: HTTP, SOCKS5
     */
    public void setProxyType(String proxyType) {
        this.proxyType = proxyType;
    }

    /**
     * Clears proxy server values.
     */
    public void clearProxySettings() {
        setProxyAuthentication(null, null);
        setProxyHost(null, DEFAULT_PROXY_PORT);
        setProxyType(DEFAULT_PROXY_TYPE);
    }

    /**
     * Adds Nntp event listener.
     *
     * @param listener a <CODE>NntpListener</CODE>
     *
     * @see NntpListener
     */
    public synchronized void addNntpListener(NntpListener listener) {
        listeners.addElement(listener);
    }

    /**
     * Removes specified Nntp event listener.
     *
     * @param listener the listener to remove
     *
     * @see NntpListener
     */
    public synchronized void removeNntpListener(NntpListener listener) {
        if (listeners.contains(listener)) {
            listeners.removeElement(listener);
        }
    }

    /**
     * Fires <code>NntpConnectedEvent</code>. This event is fired upon
     * connection to NNTP server.
     *
     * @see NntpConnectedEvent
     */
    protected void fireNntpConnected() {
        Vector thisList = new Vector();
        NntpConnectedEvent thisEvent = new NntpConnectedEvent(this,
                                                              getHostname());

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((NntpListener) thisList.elementAt(elem)).connected(thisEvent);
        }
    }

    /**
     * Fires <code>NntpDisconnectedEvent</code>. This event is fired upon
     * disconnecting from NNTP server.
     *
     * @see NntpDisconnectedEvent
     */
    protected void fireNntpDisconnected() {
        Vector thisList = new Vector();
        NntpDisconnectedEvent thisEvent = new NntpDisconnectedEvent(this,
                                                                    getHostname());

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((NntpListener) thisList.elementAt(elem)).disconnected(thisEvent);
        }
    }

    /**
     * Fires <code>NntpArticlePostedEvent</code>. This event is fired when
     * article is posted to NNTP server.
     *
     * @param a the article posted
     *
     * @see NntpArticlePostedEvent
     */
    protected void fireNntpArticlePosted(NntpArticle a) {
        Vector thisList = new Vector();
        NntpArticlePostedEvent thisEvent = new NntpArticlePostedEvent(this, a);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((NntpListener) thisList.elementAt(elem)).articlePosted(thisEvent);
        }
    }

    /**
     * Fires <code>NntpArticleEvent</code>. This event is fired for each
     * article retrieved from NNTP server.
     *
     * @param a the article retrieved
     *
     * @see NntpArticleEvent
     */
    protected void fireNntpArticleRetrieved(NntpArticle a) {
        Vector thisList = new Vector();
        NntpArticleEvent thisEvent = new NntpArticleEvent(this, a);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((NntpListener) thisList.elementAt(elem)).articleRetrieved(thisEvent);
        }
    }

    /**
     * Fires <code>NntpArticleHeaderEvent</code>. This event is fired for each
     * article header retrieved from NNTP server.
     *
     * @param a the article header retrieved
     *
     * @see NntpArticleHeaderEvent
     */
    protected void fireNntpArticleHeaderRetrieved(NntpArticleHeader a) {
        Vector thisList = new Vector();
        NntpArticleHeaderEvent thisEvent = new NntpArticleHeaderEvent(this, a);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((NntpListener) thisList.elementAt(elem)).articleHeaderRetrieved(thisEvent);
        }
    }

    /**
     * Fires <code>NntpProgressEvent</code>. This event is fired when
     * performing lengthy operations such downloading a list of available
     * newsgroups or retrieving a large number of newgroup articles.
     *
     * @param a the number of articles or newsgroups retrieved
     *
     * @see NntpProgressEvent
     */
    protected void fireNntpProgress(int a) {
        Vector thisList = new Vector();
        NntpProgressEvent thisEvent = new NntpProgressEvent(this, a);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((NntpListener) thisList.elementAt(elem)).progress(thisEvent);
        }
    }

    /**
     * Parses response code from response string with avoiding cryptic
     * exceptions in case of invalid/garbled response string.
     *
     * @param response the unparsed response received from the NNTP server
     *
     * @return the response code
     *
     * @throws NntpException thrown for I/O or Nntp related errors
     */
    protected int code(String response)
            throws NntpException {
        int c = RESPONSE_PROGRAM_FAULT;

        try {
            c = Integer.parseInt(response.substring(0, response.indexOf(" ")));
        } catch (Exception e) {
            throw new NntpException("Invalid server response, response code could not be parsed: " + response);
        }

        return c;
    }

    /**
     * Returns server response string, the string after server response code.
     *
     * @param response the unparsed response from the NNTP server
     *
     * @return the message portion of response
     */
    protected String message(String response) {
        return response.substring(response.indexOf(" ") + 1);
    }

    /**
     * Gets date string in format required by NNTP server. Note: the returned
     * string will start with space ' ' character.
     *
     * @param d the Date to translate
     *
     * @return formatted date
     */
    private String getNntpDate(Date d) {
        SimpleDateFormat format = new SimpleDateFormat(" yyMMdd HHmmss");

        return format.format(d);
    }

    /**
     * Reads article ID's as returned by server responding to NEWNEWS command.
     *
     * @return a set of article ID's
     *
     * @throws IOException if an I/O related error occurs
     */
    private String[] readIDList()
            throws IOException {
        Vector v = new Vector();
        String a = reader.readLine();

        while (!a.equals(".")) {
            v.addElement(a);
            a = reader.readLine();
            this.fireNntpProgress(v.size());
        }

        String[] ret = new String[v.size()];

        v.copyInto(ret);

        return ret;
    }

    /**
     * Sets the timeout for opening a NNTP server connection.
     * Default is 30 seconds.
     *
     * @param timeout in milliseconds
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    /**
     * Gets the timeout used when opening a NNTP server connection.
     * Default is 30 seconds.
     *
     * @return timeout in milliseconds
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Sends command to NNTP server.
     *
     * @param command the command to send
     *
     * @return response from NNTP server
     *
     * @throws NntpException if an I/O or NNTP related error occurs
     */
    public String issueCommand(String command)
            throws NntpException {
        String response = null;

        try {
            command = command + NEWLINE;
            debug(command);
            writer.write(command.getBytes());
            writer.flush();
            response = reader.readLine();
            debug(response);
            serverResponse.addElement(response);

            return response;
        } catch (Exception e) {
            throw new NntpException(e.getMessage(), e);
        }
    }

    /**
     * Set debugging state. Debugging information is sent to System.out
     *
     * @param debug <code>true</code> turns debugging on
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Get debugging state.
     *
     * @return <code>true</code> if debugging is on
     */
    public boolean getDebug() {
        return debug;
    }

    /**
     * Sends debug messages to System.out.
     *
     * @param msg the message
     */
    protected void debug(String msg) {
        if (debug) {
            debugStream.println(msg);
        }
    }

    /**
     * Gets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out
     *
     * @return the PrintSteam used in debugging
     *
     * @see java.io.PrintStream
     */
    public PrintStream getDebugStream() {
        return this.debugStream;
    }

    /**
     * Sets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out
     *
     * @param debugStream the PrintStream to send debug statements to
     *
     * @see java.io.PrintStream
     */
    public void setDebugStream(PrintStream debugStream) {
        this.debugStream = debugStream;
    }
}
