FileDocCategorySizeDatePackage
NNTPHandler.javaAPI DocApache James 2.3.161844Fri Jan 12 12:56:24 GMT 2007org.apache.james.nntpserver

NNTPHandler

public class NNTPHandler extends org.apache.avalon.framework.logger.AbstractLogEnabled implements org.apache.avalon.cornerstone.services.connection.ConnectionHandler, org.apache.avalon.excalibur.pool.Poolable
The NNTP protocol is defined by RFC 977. This implementation is based on IETF draft 15, posted on 15th July '2002. URL: http://www.ietf.org/internet-drafts/draft-ietf-nntpext-base-15.txt Common NNTP extensions are in RFC 2980.

Fields Summary
private static final org.apache.mailet.dates.SimplifiedDateFormat
DF_RFC977
used to calculate DATE from - see 11.3
private static final org.apache.mailet.dates.SimplifiedDateFormat
DF_RFC2980
Date format for the DATE keyword - see 11.1.1
public static final long
UTC_OFFSET
The UTC offset for this time zone.
private static final String
COMMAND_MODE
The text string for the NNTP MODE command.
private static final String
COMMAND_LIST
The text string for the NNTP LIST command.
private static final String
COMMAND_GROUP
The text string for the NNTP GROUP command.
private static final String
COMMAND_NEXT
The text string for the NNTP NEXT command.
private static final String
COMMAND_LAST
The text string for the NNTP LAST command.
private static final String
COMMAND_ARTICLE
The text string for the NNTP ARTICLE command.
private static final String
COMMAND_HEAD
The text string for the NNTP HEAD command.
private static final String
COMMAND_BODY
The text string for the NNTP BODY command.
private static final String
COMMAND_STAT
The text string for the NNTP STAT command.
private static final String
COMMAND_POST
The text string for the NNTP POST command.
private static final String
COMMAND_IHAVE
The text string for the NNTP IHAVE command.
private static final String
COMMAND_QUIT
The text string for the NNTP QUIT command.
private static final String
COMMAND_SLAVE
The text string for the NNTP SLAVE command.
private static final String
COMMAND_DATE
The text string for the NNTP DATE command.
private static final String
COMMAND_HELP
The text string for the NNTP HELP command.
private static final String
COMMAND_NEWGROUPS
The text string for the NNTP NEWGROUPS command.
private static final String
COMMAND_NEWNEWS
The text string for the NNTP NEWNEWS command.
private static final String
COMMAND_LISTGROUP
The text string for the NNTP LISTGROUP command.
private static final String
COMMAND_OVER
The text string for the NNTP OVER command.
private static final String
COMMAND_XOVER
The text string for the NNTP XOVER command.
private static final String
COMMAND_HDR
The text string for the NNTP HDR command.
private static final String
COMMAND_XHDR
The text string for the NNTP XHDR command.
private static final String
COMMAND_AUTHINFO
The text string for the NNTP AUTHINFO command.
private static final String
COMMAND_PAT
The text string for the NNTP PAT command.
private static final String
MODE_TYPE_READER
The text string for the NNTP MODE READER parameter.
private static final String
MODE_TYPE_STREAM
The text string for the NNTP MODE STREAM parameter.
private static final String
AUTHINFO_PARAM_USER
The text string for the NNTP AUTHINFO USER parameter.
private static final String
AUTHINFO_PARAM_PASS
The text string for the NNTP AUTHINFO PASS parameter.
private static final char[]
NNTPTerminator
The character array that indicates termination of an NNTP message
private Thread
handlerThread
The thread executing this handler
private String
remoteHost
The remote host name obtained by lookup on the socket.
private String
remoteIP
The remote IP address of the socket.
private Socket
socket
The TCP/IP socket over which the POP3 interaction is occurring
private InputStream
in
The incoming stream of bytes coming from the socket.
private BufferedReader
reader
The reader associated with incoming characters.
private OutputStream
outs
The socket's output stream
private PrintWriter
writer
The writer to which outgoing messages are written.
private org.apache.james.nntpserver.repository.NNTPGroup
group
The current newsgroup.
private int
currentArticleNumber
The current newsgroup.
private NNTPHandlerConfigurationData
theConfigData
Per-service configuration data that applies to all handlers associated with the service.
private String
user
The user id associated with the NNTP dialogue
private String
password
The password associated with the NNTP dialogue
boolean
isAlreadyAuthenticated
Whether the user for this session has already authenticated. Used to optimize authentication checks
private org.apache.james.util.watchdog.Watchdog
theWatchdog
The watchdog being used by this handler to deal with idle timeouts.
private org.apache.james.util.watchdog.WatchdogTarget
theWatchdogTarget
The watchdog target that idles out this handler.
Constructors Summary
Methods Summary
private voidcreateArticle()
Handles the transaction for getting the article data.

        try {
            InputStream msgIn = new CharTerminatedInputStream(in, NNTPTerminator);
            // Removes the dot stuffing
            msgIn = new DotStuffingInputStream(msgIn);
            MailHeaders headers = new MailHeaders(msgIn);
            processMessageHeaders(headers);
            processMessage(headers, msgIn);
        } catch (MessagingException me) {
            throw new NNTPException("MessagingException encountered when loading article.");
        }
    
private voiddoARTICLE(java.lang.String param)
Executes the ARTICLE command. Sets the current article pointer, returns article information and contents.

param
the argument passed in to the ARTICLE command, which should be an article number or message id. If no parameter is provided, the current selected article is used.

        // section 9.2.1
        NNTPArticle article = null;
        if (isMessageId(param)) {
            article = theConfigData.getNNTPRepository().getArticleFromID(param);
            if ( article == null ) {
                writeLoggedFlushedResponse("430 no such article");
                return;
            } else {
                StringBuffer respBuffer =
                    new StringBuffer(64)
                            .append("220 0 ")
                            .append(param);
                writeLoggedResponse(respBuffer.toString());
            }
        } else {
            int newArticleNumber = currentArticleNumber;
            if ( group == null ) {
                writeLoggedFlushedResponse("412 no newsgroup selected");
                return;
            } else {
                if ( param == null ) {
                    if ( currentArticleNumber < 0 ) {
                        writeLoggedFlushedResponse("420 no current article selected");
                        return;
                    } else {
                        article = group.getArticle(currentArticleNumber);
                    }
                }
                else {
                    newArticleNumber = Integer.parseInt(param);
                    article = group.getArticle(newArticleNumber);
                }
                if ( article == null ) {
                    writeLoggedFlushedResponse("423 no such article number in this group");
                    return;
                } else {
                    currentArticleNumber = newArticleNumber;
                    String articleID = article.getUniqueID();
                    if (articleID == null) {
                        articleID = "<0>";
                    }
                    StringBuffer respBuffer =
                        new StringBuffer(128)
                                .append("220 ")
                                .append(article.getArticleNumber())
                                .append(" ")
                                .append(articleID);
                    writeLoggedFlushedResponse(respBuffer.toString());
                }
            }
        }
        if (article != null) {
            writer.flush();
            article.writeArticle(new ExtraDotOutputStream(outs));
            // see jira JAMES-311 for an explanation of the "\r\n"
            writeLoggedFlushedResponse("\r\n.");
        }
    
private voiddoAUTHINFO(java.lang.String argument)
Implements only the originnal AUTHINFO. for simple and generic AUTHINFO, 501 is sent back. This is as per article 3.1.3 of RFC 2980

param
argument the argument passed in with the AUTHINFO command

        String command = null;
        String value = null;
        if (argument != null) {
            int spaceIndex = argument.indexOf(" ");
            if (spaceIndex >= 0) {
                command = argument.substring(0, spaceIndex);
                value = argument.substring(spaceIndex + 1);
            }
        }
        if (command == null) {
            writeLoggedFlushedResponse("501 Syntax error");
            return;
        }
        command = command.toUpperCase(Locale.US);
        if ( command.equals(AUTHINFO_PARAM_USER) ) {
            // Reject re-authentication
            if ( isAlreadyAuthenticated ) {
                writeLoggedFlushedResponse("482 Already authenticated - rejecting new credentials");
            }
            // Reject doubly sent user
            if (user != null) {
                user = null;
                password = null;
                isAlreadyAuthenticated = false;
                writeLoggedFlushedResponse("482 User already specified - rejecting new user");
                return;
            }
            user = value;
            writeLoggedFlushedResponse("381 More authentication information required");
        } else if ( command.equals(AUTHINFO_PARAM_PASS) ) {
            // Reject password sent before user
            if (user == null) {
                writeLoggedFlushedResponse("482 User not yet specified.  Rejecting user.");
                return;
            }
            // Reject doubly sent password
            if (password != null) {
                user = null;
                password = null;
                isAlreadyAuthenticated = false;
                writeLoggedFlushedResponse("482 Password already specified - rejecting new password");
                return;
            }
            password = value;
            isAlreadyAuthenticated = isAuthenticated();
            if ( isAlreadyAuthenticated ) {
                writeLoggedFlushedResponse("281 Authentication accepted");
            } else {
                writeLoggedFlushedResponse("482 Authentication rejected");
                // Clear bad authentication
                user = null;
                password = null;
            }
        } else {
            writeLoggedFlushedResponse("501 Syntax error");
            return;
        }
    
private voiddoBODY(java.lang.String param)
Executes the BODY command. Sets the current article pointer, returns article information and body.

param
the argument passed in to the BODY command, which should be an article number or message id. If no parameter is provided, the current selected article is used.

        // section 9.2.3
        NNTPArticle article = null;
        if (isMessageId(param)) {
            article = theConfigData.getNNTPRepository().getArticleFromID(param);
            if ( article == null ) {
                writeLoggedFlushedResponse("430 no such article");
                return;
            } else {
                StringBuffer respBuffer =
                    new StringBuffer(64)
                            .append("222 0 ")
                            .append(param);
                writeLoggedFlushedResponse(respBuffer.toString());
            }
        } else {
            int newArticleNumber = currentArticleNumber;
            if ( group == null ) {
                writeLoggedFlushedResponse("412 no newsgroup selected");
                return;
            } else {
                if ( param == null ) {
                    if ( currentArticleNumber < 0 ) {
                        writeLoggedFlushedResponse("420 no current article selected");
                        return;
                    } else {
                        article = group.getArticle(currentArticleNumber);
                    }
                }
                else {
                    newArticleNumber = Integer.parseInt(param);
                    article = group.getArticle(newArticleNumber);
                }
                if ( article == null ) {
                    writeLoggedFlushedResponse("423 no such article number in this group");
                    return;
                } else {
                    currentArticleNumber = newArticleNumber;
                    String articleID = article.getUniqueID();
                    if (articleID == null) {
                        articleID = "<0>";
                    }
                    StringBuffer respBuffer =
                        new StringBuffer(128)
                                .append("222 ")
                                .append(article.getArticleNumber())
                                .append(" ")
                                .append(articleID);
                    writeLoggedFlushedResponse(respBuffer.toString());
                }
            }
        }
        if (article != null) {
            writer.flush();
            article.writeBody(new ExtraDotOutputStream(outs));
            writeLoggedFlushedResponse("\r\n.");
        }
    
private voiddoDATE(java.lang.String argument)
Returns the current date according to the news server.

param
argument the argument passed in with the DATE command

        Date dt = new Date(System.currentTimeMillis()-UTC_OFFSET);
        String dtStr = DF_RFC2980.format(new Date(dt.getTime() - UTC_OFFSET));
        writeLoggedFlushedResponse("111 " + dtStr);
    
private voiddoGROUP(java.lang.String groupName)
Selects a group to be the current newsgroup.

param
group the name of the group being selected.

        if (groupName == null) {
            writeLoggedFlushedResponse("501 Syntax error - missing required parameter");
            return;
        }
        NNTPGroup newGroup = theConfigData.getNNTPRepository().getGroup(groupName);
        // section 9.1.1.1
        if ( newGroup == null ) {
            writeLoggedFlushedResponse("411 no such newsgroup");
        } else {
            group = newGroup;
            // if the number of articles in group == 0
            // then the server may return this information in 3 ways, 
            // The clients must honor all those 3 ways.
            // our response is: 
            // highWaterMark == lowWaterMark and number of articles == 0
            int articleCount = group.getNumberOfArticles();
            int lowWaterMark = group.getFirstArticleNumber();
            int highWaterMark = group.getLastArticleNumber();

            // Set the current article pointer.  If no
            // articles are in the group, the current article
            // pointer should be explicitly unset.
            if (articleCount != 0) {
                currentArticleNumber = lowWaterMark;
            } else {
                currentArticleNumber = -1;
            }
            StringBuffer respBuffer =
                new StringBuffer(128)
                        .append("211 ")
                        .append(articleCount)
                        .append(" ")
                        .append(lowWaterMark)
                        .append(" ")
                        .append(highWaterMark)
                        .append(" ")
                        .append(group.getName())
                        .append(" group selected");
            writeLoggedFlushedResponse(respBuffer.toString());
        }
    
private voiddoHDR(java.lang.String argument)
Get the values of the headers for the selected newsgroup, with an optional range modifier.

param
argument the argument passed in with the HDR command.

        // 9.5.3
        if (argument == null) {
            writeLoggedFlushedResponse("501 Syntax error - missing required parameter");
            return;
        }
        String hdr = argument;
        String range = null;
        int spaceIndex = hdr.indexOf(" ");
        if (spaceIndex >= 0 ) {
            range = hdr.substring(spaceIndex + 1);
            hdr = hdr.substring(0, spaceIndex);
        }
        if (group == null ) {
            writeLoggedFlushedResponse("412 No news group currently selected.");
            return;
        }
        if ((range == null) && (currentArticleNumber < 0)) {
            writeLoggedFlushedResponse("420 No current article selected");
            return;
        }
        NNTPArticle[] article = getRange(range);
        if ( article == null ) {
            writeLoggedFlushedResponse("412 no newsgroup selected");
        } else if ( article.length == 0 ) {
            writeLoggedFlushedResponse("430 no such article");
        } else {
            writeLoggedFlushedResponse("221 Header follows");
            for ( int i = 0 ; i < article.length ; i++ ) {
                String val = article[i].getHeader(hdr);
                if ( val == null ) {
                    val = "";
                }
                StringBuffer hdrBuffer =
                    new StringBuffer(128)
                            .append(article[i].getArticleNumber())
                            .append(" ")
                            .append(val);
                writeLoggedResponse(hdrBuffer.toString());
            }
            writeLoggedFlushedResponse(".");
        }
    
private voiddoHEAD(java.lang.String param)
Executes the HEAD command. Sets the current article pointer, returns article information and headers.

param
the argument passed in to the HEAD command, which should be an article number or message id. If no parameter is provided, the current selected article is used.

        // section 9.2.2
        NNTPArticle article = null;
        if (isMessageId(param)) {
            article = theConfigData.getNNTPRepository().getArticleFromID(param);
            if ( article == null ) {
                writeLoggedFlushedResponse("430 no such article");
                return;
            } else {
                StringBuffer respBuffer =
                    new StringBuffer(64)
                            .append("221 0 ")
                            .append(param);
                writeLoggedFlushedResponse(respBuffer.toString());
            }
        } else {
            int newArticleNumber = currentArticleNumber;
            if ( group == null ) {
                writeLoggedFlushedResponse("412 no newsgroup selected");
                return;
            } else {
                if ( param == null ) {
                    if ( currentArticleNumber < 0 ) {
                        writeLoggedFlushedResponse("420 no current article selected");
                        return;
                    } else {
                        article = group.getArticle(currentArticleNumber);
                    }
                }
                else {
                    newArticleNumber = Integer.parseInt(param);
                    article = group.getArticle(newArticleNumber);
                }
                if ( article == null ) {
                    writeLoggedFlushedResponse("423 no such article number in this group");
                    return;
                } else {
                    currentArticleNumber = newArticleNumber;
                    String articleID = article.getUniqueID();
                    if (articleID == null) {
                        articleID = "<0>";
                    }
                    StringBuffer respBuffer =
                        new StringBuffer(128)
                                .append("221 ")
                                .append(article.getArticleNumber())
                                .append(" ")
                                .append(articleID);
                    writeLoggedFlushedResponse(respBuffer.toString());
                }
            }
        }
        if (article != null) {
            writer.flush();
            article.writeHead(new ExtraDotOutputStream(outs));
            writeLoggedFlushedResponse(".");
        }
    
private voiddoHELP(java.lang.String argument)
Lists the help text for the service.

param
argument the argument passed in with the HELP command.

        writeLoggedResponse("100 Help text follows");
        writeLoggedFlushedResponse(".");
    
private voiddoIHAVE(java.lang.String id)
Informs the server that the client has an article with the specified message-ID.

param
id the message id

        // see section 9.3.2.1
        if (!isMessageId(id)) {
            writeLoggedFlushedResponse("501 command syntax error");
            return;
        }
        NNTPArticle article = theConfigData.getNNTPRepository().getArticleFromID(id);
        if ( article != null ) {
            writeLoggedFlushedResponse("435 article not wanted - do not send it");
        } else {
            writeLoggedFlushedResponse("335 send article to be transferred. End with <CR-LF>.<CR-LF>");
            try {
                createArticle();
            } catch (RuntimeException e) {
                writeLoggedFlushedResponse("436 transfer failed - try again later");
                throw e;
            }
            writeLoggedFlushedResponse("235 article received ok");
        }
    
private voiddoLAST(java.lang.String argument)
Moves the currently selected article pointer to the article previous to the currently selected article in the selected group.

param
argument the argument passed in with the LAST command

        // section 9.1.1.2.1
        if ( argument != null ) {
            writeLoggedFlushedResponse("501 Syntax error - unexpected parameter");
        } else if ( group == null ) {
            writeLoggedFlushedResponse("412 no newsgroup selected");
        } else if ( currentArticleNumber < 0 ) {
            writeLoggedFlushedResponse("420 no current article has been selected");
        } else if ( currentArticleNumber <= group.getFirstArticleNumber() ) {
            writeLoggedFlushedResponse("422 no previous article in this group");
        } else {
            currentArticleNumber--;
            NNTPArticle article = group.getArticle(currentArticleNumber);
            StringBuffer respBuffer =
                new StringBuffer(64)
                        .append("223 ")
                        .append(article.getArticleNumber())
                        .append(" ")
                        .append(article.getUniqueID());
            writeLoggedFlushedResponse(respBuffer.toString());
        }
    
private voiddoLIST(java.lang.String argument)
Handles the LIST command and its assorted extensions.

param
argument the argument passed in with the LIST command.

        // see section 9.4.1
        String wildmat = "*";
        boolean isListNewsgroups = false;

        String extension = argument;
        if (argument != null) {
            int spaceIndex = argument.indexOf(" ");
            if (spaceIndex >= 0) {
                wildmat = argument.substring(spaceIndex + 1);
                extension = argument.substring(0, spaceIndex);
            }
            extension = extension.toUpperCase(Locale.US);
        }

        if (extension != null) {
            if (extension.equals("ACTIVE")) {
                isListNewsgroups = false;
            } else if (extension.equals("NEWSGROUPS") ) {
                isListNewsgroups = true;
            } else if (extension.equals("EXTENSIONS") ) {
                doLISTEXTENSIONS();
                return;
            } else if (extension.equals("OVERVIEW.FMT") ) {
                doLISTOVERVIEWFMT();
                return;
            } else if (extension.equals("ACTIVE.TIMES") ) {
                // not supported - 9.4.2.1, 9.4.3.1, 9.4.4.1
                writeLoggedFlushedResponse("503 program error, function not performed");
                return;
            } else if (extension.equals("DISTRIBUTIONS") ) {
                // not supported - 9.4.2.1, 9.4.3.1, 9.4.4.1
                writeLoggedFlushedResponse("503 program error, function not performed");
                return;
            } else if (extension.equals("DISTRIB.PATS") ) {
                // not supported - 9.4.2.1, 9.4.3.1, 9.4.4.1
                writeLoggedFlushedResponse("503 program error, function not performed");
                return;
            } else {
                writeLoggedFlushedResponse("501 Syntax error");
                return;
            }
        }

        Iterator iter = theConfigData.getNNTPRepository().getMatchedGroups(wildmat);
        writeLoggedFlushedResponse("215 list of newsgroups follows");
        while ( iter.hasNext() ) {
            NNTPGroup theGroup = (NNTPGroup)iter.next();
            if (isListNewsgroups) {
                writeLoggedResponse(theGroup.getListNewsgroupsFormat());
            } else {
                writeLoggedResponse(theGroup.getListFormat());
            }
        }
        writeLoggedFlushedResponse(".");
    
private voiddoLISTEXTENSIONS()
Lists the extensions supported by this news server.

        // 8.1.1
        writeLoggedResponse("202 Extensions supported:");
        writeLoggedResponse("LISTGROUP");
        writeLoggedResponse("AUTHINFO");
        writeLoggedResponse("OVER");
        writeLoggedResponse("XOVER");
        writeLoggedResponse("HDR");
        writeLoggedResponse("XHDR");
        writeLoggedFlushedResponse(".");
    
private voiddoLISTGROUP(java.lang.String groupName)
Gets a listing of article numbers in specified group name or in the already selected group if the groupName is null.

param
groupName the name of the group to list

        // 9.5.1.1.1
        if (groupName==null) {
            if ( group == null) {
                writeLoggedFlushedResponse("412 no news group currently selected");
                return;
            }
        }
        else {
            group = theConfigData.getNNTPRepository().getGroup(groupName);
            if ( group == null ) {
                writeLoggedFlushedResponse("411 no such newsgroup");
                return;
            }
        }
        if ( group != null ) {
            // this.group = group;

            // Set the current article pointer.  If no
            // articles are in the group, the current article
            // pointer should be explicitly unset.
            if (group.getNumberOfArticles() > 0) {
                currentArticleNumber = group.getFirstArticleNumber();
            } else {
                currentArticleNumber = -1;
            }

            writeLoggedFlushedResponse("211 list of article numbers follow");

            Iterator iter = group.getArticles();
            while (iter.hasNext()) {
                NNTPArticle article = (NNTPArticle)iter.next();
                writeLoggedResponse(article.getArticleNumber() + "");
            }
            writeLoggedFlushedResponse(".");
        }
    
private voiddoLISTOVERVIEWFMT()
Handles the LIST OVERVIEW.FMT command. Not supported.

        // 9.5.3.1.1
        writeLoggedFlushedResponse("215 Information follows");
        String[] overviewHeaders = theConfigData.getNNTPRepository().getOverviewFormat();
        for (int i = 0;  i < overviewHeaders.length; i++) {
            writeLoggedResponse(overviewHeaders[i]);
        }
        writeLoggedFlushedResponse(".");
    
private voiddoMODEREADER(java.lang.String argument)
Informs the server that the client is a newsreader.

param
argument the argument passed in with the MODE READER command

        // 7.2
        writeLoggedFlushedResponse(theConfigData.getNNTPRepository().isReadOnly()
                       ? "201 Posting Not Permitted" : "200 Posting Permitted");
    
private voiddoMODESTREAM(java.lang.String argument)
Informs the server that the client is a news server.

param
argument the argument passed in with the MODE STREAM command

        // 7.2
        writeLoggedFlushedResponse("500 Command not understood");
    
private voiddoNEWGROUPS(java.lang.String argument)
Lists the groups added since the date passed in as an argument.

param
argument the argument passed in with the NEWGROUPS command. Should be a date.

        // see section 11.3
        // both draft-ietf-nntpext-base-15.txt and rfc977 have only group names 
        // in response lines, but INN sends 
        // '<group name> <last article> <first article> <posting allowed>'
        // NOTE: following INN over either document.
        //
        // TODO: Check this.  Audit at http://www.academ.com/pipermail/ietf-nntp/2001-July/002185.html
        // doesn't mention the supposed discrepancy.  Consider changing code to 
        // be in line with spec.
        Date theDate = null;
        try {
            theDate = getDateFrom(argument);
        } catch (NNTPException nntpe) {
            getLogger().error("NEWGROUPS had an invalid argument", nntpe);
            writeLoggedFlushedResponse("501 Syntax error");
            return;
        }
        Iterator iter = theConfigData.getNNTPRepository().getGroupsSince(theDate);
        writeLoggedFlushedResponse("231 list of new newsgroups follows");
        while ( iter.hasNext() ) {
            NNTPGroup currentGroup = (NNTPGroup)iter.next();
            StringBuffer iterBuffer =
                new StringBuffer(128)
                    .append(currentGroup.getName())
                    .append(" ")
                    .append(currentGroup.getLastArticleNumber())
                    .append(" ")
                    .append(currentGroup.getFirstArticleNumber())
                    .append(" ")
                    .append((currentGroup.isPostAllowed()?"y":"n"));
            writeLoggedResponse(iterBuffer.toString());
        }
        writeLoggedFlushedResponse(".");
    
private voiddoNEWNEWS(java.lang.String argument)
Lists the articles posted since the date passed in as an argument.

param
argument the argument passed in with the NEWNEWS command. Should be a wildmat followed by a date.

        // see section 11.4

        String wildmat = "*";

        if (argument != null) {
            int spaceIndex = argument.indexOf(" ");
            if (spaceIndex >= 0) {
                wildmat = argument.substring(0, spaceIndex);
                argument = argument.substring(spaceIndex + 1);
            } else {
                getLogger().error("NEWNEWS had an invalid argument");
                writeLoggedFlushedResponse("501 Syntax error");
                return;
            }
        } else {
            getLogger().error("NEWNEWS had a null argument");
            writeLoggedFlushedResponse("501 Syntax error");
            return;
        }

        Date theDate = null;
        try {
            theDate = getDateFrom(argument);
        } catch (NNTPException nntpe) {
            getLogger().error("NEWNEWS had an invalid argument", nntpe);
            writeLoggedFlushedResponse("501 Syntax error");
            return;
        }

        writeLoggedFlushedResponse("230 list of new articles by message-id follows");
        Iterator groupIter = theConfigData.getNNTPRepository().getMatchedGroups(wildmat);
        while ( groupIter.hasNext() ) {
            Iterator articleIter = ((NNTPGroup)(groupIter.next())).getArticlesSince(theDate);
            while (articleIter.hasNext()) {
                StringBuffer iterBuffer =
                    new StringBuffer(64)
                        .append(((NNTPArticle)articleIter.next()).getUniqueID());
                writeLoggedResponse(iterBuffer.toString());
            }
        }
        writeLoggedFlushedResponse(".");
    
private voiddoNEXT(java.lang.String argument)
Advances the current article pointer to the next article in the group.

param
argument the argument passed in with the NEXT command

        // section 9.1.1.3.1
        if ( argument != null ) {
            writeLoggedFlushedResponse("501 Syntax error - unexpected parameter");
        } else if ( group == null ) {
            writeLoggedFlushedResponse("412 no newsgroup selected");
        } else if ( currentArticleNumber < 0 ) {
            writeLoggedFlushedResponse("420 no current article has been selected");
        } else if ( currentArticleNumber >= group.getLastArticleNumber() ) {
            writeLoggedFlushedResponse("421 no next article in this group");
        } else {
            currentArticleNumber++;
            NNTPArticle article = group.getArticle(currentArticleNumber);
            StringBuffer respBuffer =
                new StringBuffer(64)
                        .append("223 ")
                        .append(article.getArticleNumber())
                        .append(" ")
                        .append(article.getUniqueID());
            writeLoggedFlushedResponse(respBuffer.toString());
        }
    
private voiddoOVER(java.lang.String range)
Returns information from the overview database regarding the current article, or a range of articles.

param
range the optional article range.

        // 9.5.2.2.1
        if ( group == null ) {
            writeLoggedFlushedResponse("412 No newsgroup selected");
            return;
        }
        if ((range == null) && (currentArticleNumber < 0)) {
            writeLoggedFlushedResponse("420 No current article selected");
            return;
        }
        NNTPArticle[] article = getRange(range);
        if ( article.length == 0 ) {
            writeLoggedFlushedResponse("420 No article(s) selected");
        } else {
            writeLoggedResponse("224 Overview information follows");
            for ( int i = 0 ; i < article.length ; i++ ) {
                article[i].writeOverview(outs);
                if (i % 100 == 0) {
                    // Reset the watchdog every hundred headers or so
                    // to ensure the connection doesn't timeout for slow
                    // clients
                    theWatchdog.reset();
                }
            }
            writeLoggedFlushedResponse(".");
        }
    
private voiddoPAT(java.lang.String argument)
Handles the PAT command. Not supported.

param
argument the argument passed in with the PAT command

        // 9.5.3.1.1 in draft-12
        writeLoggedFlushedResponse("500 Command not recognized");
    
private voiddoPOST(java.lang.String argument)
Posts an article to the news server.

param
argument the argument passed in with the POST command

        // see section 9.3.1.1
        if ( argument != null ) {
            writeLoggedFlushedResponse("501 Syntax error - unexpected parameter");
        }
        writeLoggedFlushedResponse("340 send article to be posted. End with <CR-LF>.<CR-LF>");
        createArticle();
        writeLoggedFlushedResponse("240 article received ok");
    
private voiddoQUIT(java.lang.String argument)
Quits the transaction.

param
argument the argument passed in with the QUIT command

        writeLoggedFlushedResponse("205 closing connection");
    
private voiddoSLAVE(java.lang.String argument)
Acknowledges a SLAVE command. No special preference is given to slave connections.

param
argument the argument passed in with the SLAVE command.

        writeLoggedFlushedResponse("202 slave status noted");
    
private voiddoSTAT(java.lang.String param)
Executes the STAT command. Sets the current article pointer, returns article information.

param
the argument passed in to the STAT command, which should be an article number or message id. If no parameter is provided, the current selected article is used.

        // section 9.2.4
        NNTPArticle article = null;
        if (isMessageId(param)) {
            article = theConfigData.getNNTPRepository().getArticleFromID(param);
            if ( article == null ) {
                writeLoggedFlushedResponse("430 no such article");
                return;
            } else {
                StringBuffer respBuffer =
                    new StringBuffer(64)
                            .append("223 0 ")
                            .append(param);
                writeLoggedFlushedResponse(respBuffer.toString());
            }
        } else {
            int newArticleNumber = currentArticleNumber;
            if ( group == null ) {
                writeLoggedFlushedResponse("412 no newsgroup selected");
                return;
            } else {
                if ( param == null ) {
                    if ( currentArticleNumber < 0 ) {
                        writeLoggedFlushedResponse("420 no current article selected");
                        return;
                    } else {
                        article = group.getArticle(currentArticleNumber);
                    }
                }
                else {
                    newArticleNumber = Integer.parseInt(param);
                    article = group.getArticle(newArticleNumber);
                }
                if ( article == null ) {
                    writeLoggedFlushedResponse("423 no such article number in this group");
                    return;
                } else {
                    currentArticleNumber = newArticleNumber;
                    String articleID = article.getUniqueID();
                    if (articleID == null) {
                        articleID = "<0>";
                    }
                    StringBuffer respBuffer =
                        new StringBuffer(128)
                                .append("223 ")
                                .append(article.getArticleNumber())
                                .append(" ")
                                .append(articleID);
                    writeLoggedFlushedResponse(respBuffer.toString());
                }
            }
        }
    
private voiddoUnknownCommand(java.lang.String command, java.lang.String argument)
Handles an unrecognized command, logging that.

param
command the command received from the client
param
argument the argument passed in with the command

        if (getLogger().isDebugEnabled()) {
            StringBuffer logBuffer =
                new StringBuffer(128)
                    .append("Received unknown command ")
                    .append(command)
                    .append(" with argument ")
                    .append(argument);
            getLogger().debug(logBuffer.toString());
        }
        writeLoggedFlushedResponse("500 Unknown command");
    
private voiddoXHDR(java.lang.String argument)
Get the values of the headers for the selected newsgroup, with an optional range modifier.

param
argument the argument passed in with the XHDR command.

        doHDR(argument);
    
private voiddoXOVER(java.lang.String range)
Returns information from the overview database regarding the current article, or a range of articles.

param
range the optional article range.

        doOVER(range);
    
private java.util.DategetDateFrom(java.lang.String argument)
Returns the date from @param input. The input tokens are assumed to be in format date time [GMT|UTC] . 'date' is in format [XX]YYMMDD. 'time' is in format 'HHMMSS' NOTE: This routine could do with some format checks.

param
argument the date string

        if (argument == null) {
            throw new NNTPException("Date argument was absent.");
        }
        StringTokenizer tok = new StringTokenizer(argument, " ");
        if (tok.countTokens() < 2) {
            throw new NNTPException("Date argument was ill-formed.");
        }
        String date = tok.nextToken();
        String time = tok.nextToken();
        boolean utc = ( tok.hasMoreTokens() );
        Date d = new Date();
        try {
            StringBuffer dateStringBuffer =
                new StringBuffer(64)
                    .append(date)
                    .append(" ")
                    .append(time);
            Date dt = DF_RFC977.parse(dateStringBuffer.toString());
            if ( utc ) {
                dt = new Date(dt.getTime()+UTC_OFFSET);
            }
            return dt;
        } catch ( ParseException pe ) {
            StringBuffer exceptionBuffer =
                new StringBuffer(128)
                    .append("Date extraction failed: ")
                    .append(date)
                    .append(",")
                    .append(time)
                    .append(",")
                    .append(utc);
            throw new NNTPException(exceptionBuffer.toString());
        }
    
private org.apache.james.nntpserver.repository.NNTPArticle[]getRange(java.lang.String range)
Returns the list of articles that match the range. A precondition of this method is that the selected group be non-null. The current article pointer must be valid if no range is explicitly provided.

return
null indicates insufficient information to fetch the list of articles

        // check for msg id
        if ( isMessageId(range)) {
            NNTPArticle article = theConfigData.getNNTPRepository().getArticleFromID(range);
            return ( article == null )
                ? new NNTPArticle[0] : new NNTPArticle[] { article };
        }

        if ( range == null ) {
            range = "" + currentArticleNumber;
        }

        int start = -1;
        int end = -1;
        int idx = range.indexOf('-");
        if ( idx == -1 ) {
            start = Integer.parseInt(range);
            end = start;
        } else {
            start = Integer.parseInt(range.substring(0,idx));
            if ( (idx + 1) == range.length() ) {
                end = group.getLastArticleNumber();
            } else {
                end = Integer.parseInt(range.substring(idx + 1));
            }
        }
        List list = new ArrayList();
        for ( int i = start ; i <= end ; i++ ) {
            NNTPArticle article = group.getArticle(i);
            if ( article != null ) {
                list.add(article);
            }
        }
        return (NNTPArticle[])list.toArray(new NNTPArticle[0]);
    
org.apache.james.util.watchdog.WatchdogTargetgetWatchdogTarget()
Gets the Watchdog Target that should be used by Watchdogs managing this connection.

return
the WatchdogTarget

        return theWatchdogTarget;
    
public voidhandleConnection(java.net.Socket connection)

see
org.apache.avalon.cornerstone.services.connection.ConnectionHandler#handleConnection(Socket)

        try {
            this.socket = connection;
            synchronized (this) {
                handlerThread = Thread.currentThread();
            }
            remoteIP = socket.getInetAddress().getHostAddress();
            remoteHost = socket.getInetAddress().getHostName();
            in = new BufferedInputStream(socket.getInputStream(), 1024);
            // An ASCII encoding can be used because all transmissions other
            // that those in the message body command are guaranteed
            // to be ASCII
            reader = new BufferedReader(new InputStreamReader(in, "ASCII"), 512);
            outs = new BufferedOutputStream(socket.getOutputStream(), 1024);
            writer = new InternetPrintWriter(outs, true);
        } catch (Exception e) {
            StringBuffer exceptionBuffer = 
                new StringBuffer(256)
                    .append("Cannot open connection from ")
                    .append(remoteHost)
                    .append(" (")
                    .append(remoteIP)
                    .append("): ")
                    .append(e.getMessage());
            String exceptionString = exceptionBuffer.toString();
            getLogger().error(exceptionString, e );
        }

        try {
            // section 7.1
            if ( theConfigData.getNNTPRepository().isReadOnly() ) {
                StringBuffer respBuffer =
                    new StringBuffer(128)
                        .append("201 ")
                        .append(theConfigData.getHelloName())
                        .append(" NNTP Service Ready, posting prohibited");
                writeLoggedFlushedResponse(respBuffer.toString());
            } else {
                StringBuffer respBuffer =
                    new StringBuffer(128)
                            .append("200 ")
                            .append(theConfigData.getHelloName())
                            .append(" NNTP Service Ready, posting permitted");
                writeLoggedFlushedResponse(respBuffer.toString());
            }

            theWatchdog.start();
            while (parseCommand(reader.readLine())) {
                theWatchdog.reset();
            }
            theWatchdog.stop();

            getLogger().info("Connection closed");
        } catch (Exception e) {
            // If the connection has been idled out, the
            // socket will be closed and null.  Do NOT
            // log the exception or attempt to send the
            // closing connection message
            if (socket != null) {
                try {
                    doQUIT(null);
                } catch (Throwable t) {}
                getLogger().error( "Exception during connection:" + e.getMessage(), e );
            }
        } finally {
            resetHandler();
        }
    
voididleClose()
Idle out this connection

        if (getLogger() != null) {
            getLogger().error("NNTP Connection has idled out.");
        }
        try {
            if (socket != null) {
                socket.close();
            }
        } catch (Exception e) {
            // ignored
        } finally {
            socket = null;
        }

        synchronized (this) {
            // Interrupt the thread to recover from internal hangs
            if (handlerThread != null) {
                handlerThread.interrupt();
                handlerThread = null;
            }
        }
    
private booleanisAuthenticated()
Return whether the connection has been authenticated.

return
whether the connection has been authenticated.

        if ( theConfigData.isAuthRequired() ) {
            if  ((user != null) && (password != null) && (theConfigData.getUsersRepository() != null)) {
                return theConfigData.getUsersRepository().test(user,password);
            } else {
                return false;
            }
        } else {
            return true;
        }
    
private booleanisAuthorized(java.lang.String command)
Return whether the user associated with the connection (possibly no user) is authorized to execute the command.

param
the command being tested
return
whether the command is authorized

        isAlreadyAuthenticated = isAlreadyAuthenticated || isAuthenticated();
        if (isAlreadyAuthenticated) {
            return true;
        }
        // some commands are authorized, even if the user is not authenticated
        boolean allowed = command.equals("AUTHINFO");
        allowed = allowed || command.equals("MODE");
        allowed = allowed || command.equals("QUIT");
        return allowed;
    
private static booleanisMessageId(java.lang.String testString)
Tests a string to see whether it is formatted as a message ID.

param
testString the string to test
return
whether the string is a candidate message ID

        if ((testString != null) &&
            (testString.startsWith("<")) &&
            (testString.endsWith(">"))) {
            return true;
        } else {
            return false;
        }
   
private final voidlogResponseString(java.lang.String responseString)
This method logs at a "DEBUG" level the response string that was sent to the SMTP client. The method is provided largely as syntactic sugar to neaten up the code base. It is declared private and final to encourage compiler inlining.

param
responseString the response string sent to the client

        if (getLogger().isDebugEnabled()) {
            getLogger().debug("Sent: " + responseString);
        }
    
private booleanparseCommand(java.lang.String commandRaw)
This method parses NNTP commands read off the wire in handleConnection. Actual processing of the command (possibly including additional back and forth communication with the client) is delegated to one of a number of command specific handler methods. The primary purpose of this method is to parse the raw command string to determine exactly which handler should be called. It returns true if expecting additional commands, false otherwise.

param
commandRaw the raw command string passed in over the socket
return
whether additional commands are expected.

        if (commandRaw == null) {
            return false;
        }
        if (getLogger().isDebugEnabled()) {
            getLogger().debug("Command received: " + commandRaw);
        }

        String command = commandRaw.trim();
        String argument = null;
        int spaceIndex = command.indexOf(" ");
        if (spaceIndex >= 0) {
            argument = command.substring(spaceIndex + 1);
            command = command.substring(0, spaceIndex);
        }
        command = command.toUpperCase(Locale.US);

        boolean returnValue = true;
        if (!isAuthorized(command) ) {
            writeLoggedFlushedResponse("480 User is not authenticated");
            getLogger().debug("Command not allowed.");
            return returnValue;
        }
        if ((command.equals(COMMAND_MODE)) && (argument != null)) {
            if (argument.toUpperCase(Locale.US).equals(MODE_TYPE_READER)) {
                doMODEREADER(argument);
            } else if (argument.toUpperCase(Locale.US).equals(MODE_TYPE_STREAM)) {
                doMODESTREAM(argument);
            } else {
                writeLoggedFlushedResponse("500 Command not understood");
            }
        } else if ( command.equals(COMMAND_LIST)) {
            doLIST(argument);
        } else if ( command.equals(COMMAND_GROUP) ) {
            doGROUP(argument);
        } else if ( command.equals(COMMAND_NEXT) ) {
            doNEXT(argument);
        } else if ( command.equals(COMMAND_LAST) ) {
            doLAST(argument);
        } else if ( command.equals(COMMAND_ARTICLE) ) {
            doARTICLE(argument);
        } else if ( command.equals(COMMAND_HEAD) ) {
            doHEAD(argument);
        } else if ( command.equals(COMMAND_BODY) ) {
            doBODY(argument);
        } else if ( command.equals(COMMAND_STAT) ) {
            doSTAT(argument);
        } else if ( command.equals(COMMAND_POST) ) {
            doPOST(argument);
        } else if ( command.equals(COMMAND_IHAVE) ) {
            doIHAVE(argument);
        } else if ( command.equals(COMMAND_QUIT) ) {
            doQUIT(argument);
            returnValue = false;
        } else if ( command.equals(COMMAND_DATE) ) {
            doDATE(argument);
        } else if ( command.equals(COMMAND_HELP) ) {
            doHELP(argument);
        } else if ( command.equals(COMMAND_NEWGROUPS) ) {
            doNEWGROUPS(argument);
        } else if ( command.equals(COMMAND_NEWNEWS) ) {
            doNEWNEWS(argument);
        } else if ( command.equals(COMMAND_LISTGROUP) ) {
            doLISTGROUP(argument);
        } else if ( command.equals(COMMAND_OVER) ) {
            doOVER(argument);
        } else if ( command.equals(COMMAND_XOVER) ) {
            doXOVER(argument);
        } else if ( command.equals(COMMAND_HDR) ) {
            doHDR(argument);
        } else if ( command.equals(COMMAND_XHDR) ) {
            doXHDR(argument);
        } else if ( command.equals(COMMAND_AUTHINFO) ) {
            doAUTHINFO(argument);
        } else if ( command.equals(COMMAND_SLAVE) ) {
            doSLAVE(argument);
        } else if ( command.equals(COMMAND_PAT) ) {
            doPAT(argument);
        } else {
            doUnknownCommand(command, argument);
        }
        return returnValue;
    
private voidprocessMessage(org.apache.james.core.MailHeaders headers, java.io.InputStream bodyIn)
Processes the NNTP message coming in off the wire. Reads the content and delivers to the spool.

param
headers the headers of the message being read
param
msgIn the stream containing the message content

        InputStream messageIn = null;
        try {
            messageIn = new SequenceInputStream(new ByteArrayInputStream(headers.toByteArray()), bodyIn);
            theConfigData.getNNTPRepository().createArticle(messageIn);
        } finally {
            if (messageIn != null) {
                try {
                    messageIn.close();
                } catch (IOException ioe) {
                    // Ignore exception on close.
                }
                messageIn = null;
            }
        }
    
private org.apache.james.core.MailHeadersprocessMessageHeaders(org.apache.james.core.MailHeaders headers)
Processes the NNTP message headers coming in off the wire.

param
headers the headers of the message being read

        return headers;
    
private voidresetHandler()
Resets the handler data to a basic state.


        // Clear the Watchdog
        if (theWatchdog != null) {
            ContainerUtil.dispose(theWatchdog);
            theWatchdog = null;
        }

        // Clear the streams
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException ioe) {
            getLogger().warn("NNTPHandler: Unexpected exception occurred while closing reader: " + ioe);
        } finally {
            reader = null;
        }

        in = null;

        if (writer != null) {
            writer.close();
            writer = null;
        }
        outs = null;

        remoteHost = null;
        remoteIP = null;
        try {
            if (socket != null) {
                socket.close();
            }
        } catch (IOException ioe) {
            getLogger().warn("NNTPHandler: Unexpected exception occurred while closing socket: " + ioe);
        } finally {
            socket = null;
        }

        synchronized (this) {
            handlerThread = null;
        }

        // Clear the selected group, article info
        group = null;
        currentArticleNumber = -1;

        // Clear the authentication info
        user = null;
        password = null;
        isAlreadyAuthenticated = false;

        // Clear the config data
        theConfigData = null;
    
voidsetConfigurationData(NNTPHandlerConfigurationData theData)
Set the configuration data for the handler

param
theData configuration data for the handler


                       
       
        theConfigData = theData;
    
voidsetWatchdog(org.apache.james.util.watchdog.Watchdog theWatchdog)
Set the Watchdog for use by this handler.

param
theWatchdog the watchdog

        this.theWatchdog = theWatchdog;
    
final voidwriteLoggedFlushedResponse(java.lang.String responseString)
Write and flush a response string. The response is also logged. Should be used for the last line of a multi-line response or for a single line response.

param
responseString the response string sent to the client

        writer.println(responseString);
        writer.flush();
        logResponseString(responseString);
    
final voidwriteLoggedResponse(java.lang.String responseString)
Write a response string. The response is also logged. Used for multi-line responses.

param
responseString the response string sent to the client

        writer.println(responseString);
        logResponseString(responseString);