View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17  
18  package org.apache.james.nntpserver;
19  
20  import org.apache.avalon.cornerstone.services.connection.ConnectionHandler;
21  import org.apache.avalon.excalibur.pool.Poolable;
22  import org.apache.avalon.framework.container.ContainerUtil;
23  import org.apache.avalon.framework.logger.AbstractLogEnabled;
24  import org.apache.james.core.MailHeaders;
25  import org.apache.james.nntpserver.repository.NNTPArticle;
26  import org.apache.james.nntpserver.repository.NNTPGroup;
27  import org.apache.james.util.CharTerminatedInputStream;
28  import org.apache.james.util.DotStuffingInputStream;
29  import org.apache.james.util.ExtraDotOutputStream;
30  import org.apache.james.util.InternetPrintWriter;
31  import org.apache.mailet.dates.RFC977DateFormat;
32  import org.apache.mailet.dates.RFC2980DateFormat;
33  import org.apache.mailet.dates.SimplifiedDateFormat;
34  import org.apache.james.util.watchdog.Watchdog;
35  import org.apache.james.util.watchdog.WatchdogTarget;
36  
37  import java.io.BufferedInputStream;
38  import java.io.BufferedOutputStream;
39  import java.io.BufferedReader;
40  import java.io.ByteArrayInputStream;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.io.InputStreamReader;
44  import java.io.OutputStream;
45  import java.io.PrintWriter;
46  import java.io.SequenceInputStream;
47  import java.net.Socket;
48  import java.text.ParseException;
49  import java.util.ArrayList;
50  import java.util.Calendar;
51  import java.util.Date;
52  import java.util.Iterator;
53  import java.util.List;
54  import java.util.Locale;
55  import java.util.StringTokenizer;
56  import javax.mail.MessagingException;
57  
58  /***
59   * The NNTP protocol is defined by RFC 977.
60   * This implementation is based on IETF draft 15, posted on 15th July '2002.
61   * URL: http://www.ietf.org/internet-drafts/draft-ietf-nntpext-base-15.txt
62   *
63   * Common NNTP extensions are in RFC 2980.
64   */
65  public class NNTPHandler
66      extends AbstractLogEnabled
67      implements ConnectionHandler, Poolable {
68  
69      /***
70       * used to calculate DATE from - see 11.3
71       */
72      private static final SimplifiedDateFormat DF_RFC977 = new RFC977DateFormat();
73  
74      /***
75       * Date format for the DATE keyword - see 11.1.1
76       */
77      private static final SimplifiedDateFormat DF_RFC2980 = new RFC2980DateFormat();
78  
79      /***
80       * The UTC offset for this time zone.
81       */
82      public static final long UTC_OFFSET = Calendar.getInstance().get(Calendar.ZONE_OFFSET);
83  
84      /***
85       * The text string for the NNTP MODE command.
86       */
87      private final static String COMMAND_MODE = "MODE";
88  
89      /***
90       * The text string for the NNTP LIST command.
91       */
92      private final static String COMMAND_LIST = "LIST";
93  
94      /***
95       * The text string for the NNTP GROUP command.
96       */
97      private final static String COMMAND_GROUP = "GROUP";
98  
99      /***
100      * The text string for the NNTP NEXT command.
101      */
102     private final static String COMMAND_NEXT = "NEXT";
103 
104     /***
105      * The text string for the NNTP LAST command.
106      */
107     private final static String COMMAND_LAST = "LAST";
108 
109     /***
110      * The text string for the NNTP ARTICLE command.
111      */
112     private final static String COMMAND_ARTICLE = "ARTICLE";
113 
114     /***
115      * The text string for the NNTP HEAD command.
116      */
117     private final static String COMMAND_HEAD = "HEAD";
118 
119     /***
120      * The text string for the NNTP BODY command.
121      */
122     private final static String COMMAND_BODY = "BODY";
123 
124     /***
125      * The text string for the NNTP STAT command.
126      */
127     private final static String COMMAND_STAT = "STAT";
128 
129     /***
130      * The text string for the NNTP POST command.
131      */
132     private final static String COMMAND_POST = "POST";
133 
134     /***
135      * The text string for the NNTP IHAVE command.
136      */
137     private final static String COMMAND_IHAVE = "IHAVE";
138 
139     /***
140      * The text string for the NNTP QUIT command.
141      */
142     private final static String COMMAND_QUIT = "QUIT";
143 
144     /***
145      * The text string for the NNTP SLAVE command.
146      */
147     private final static String COMMAND_SLAVE = "SLAVE";
148 
149     /***
150      * The text string for the NNTP DATE command.
151      */
152     private final static String COMMAND_DATE = "DATE";
153 
154     /***
155      * The text string for the NNTP HELP command.
156      */
157     private final static String COMMAND_HELP = "HELP";
158 
159     /***
160      * The text string for the NNTP NEWGROUPS command.
161      */
162     private final static String COMMAND_NEWGROUPS = "NEWGROUPS";
163 
164     /***
165      * The text string for the NNTP NEWNEWS command.
166      */
167     private final static String COMMAND_NEWNEWS = "NEWNEWS";
168 
169     /***
170      * The text string for the NNTP LISTGROUP command.
171      */
172     private final static String COMMAND_LISTGROUP = "LISTGROUP";
173 
174     /***
175      * The text string for the NNTP OVER command.
176      */
177     private final static String COMMAND_OVER = "OVER";
178 
179     /***
180      * The text string for the NNTP XOVER command.
181      */
182     private final static String COMMAND_XOVER = "XOVER";
183 
184     /***
185      * The text string for the NNTP HDR command.
186      */
187     private final static String COMMAND_HDR = "HDR";
188 
189     /***
190      * The text string for the NNTP XHDR command.
191      */
192     private final static String COMMAND_XHDR = "XHDR";
193 
194     /***
195      * The text string for the NNTP AUTHINFO command.
196      */
197     private final static String COMMAND_AUTHINFO = "AUTHINFO";
198 
199     /***
200      * The text string for the NNTP PAT command.
201      */
202     private final static String COMMAND_PAT = "PAT";
203 
204     /***
205      * The text string for the NNTP MODE READER parameter.
206      */
207     private final static String MODE_TYPE_READER = "READER";
208 
209     /***
210      * The text string for the NNTP MODE STREAM parameter.
211      */
212     private final static String MODE_TYPE_STREAM = "STREAM";
213 
214     /***
215      * The text string for the NNTP AUTHINFO USER parameter.
216      */
217     private final static String AUTHINFO_PARAM_USER = "USER";
218 
219     /***
220      * The text string for the NNTP AUTHINFO PASS parameter.
221      */
222     private final static String AUTHINFO_PARAM_PASS = "PASS";
223 
224     /***
225      * The character array that indicates termination of an NNTP message
226      */
227     private final static char[] NNTPTerminator = { '\r', '\n', '.', '\r', '\n' };
228 
229     /***
230      * The thread executing this handler 
231      */
232     private Thread handlerThread;
233 
234     /***
235      * The remote host name obtained by lookup on the socket.
236      */
237     private String remoteHost;
238 
239     /***
240      * The remote IP address of the socket.
241      */
242     private String remoteIP;
243 
244     /***
245      * The TCP/IP socket over which the POP3 interaction
246      * is occurring
247      */
248     private Socket socket;
249 
250     /***
251      * The incoming stream of bytes coming from the socket.
252      */
253     private InputStream in;
254 
255     /***
256      * The reader associated with incoming characters.
257      */
258     private BufferedReader reader;
259 
260     /***
261      * The socket's output stream
262      */
263     private OutputStream outs;
264 
265     /***
266      * The writer to which outgoing messages are written.
267      */
268     private PrintWriter writer;
269 
270     /***
271      * The current newsgroup.
272      */
273     private NNTPGroup group;
274 
275     /***
276      * The current newsgroup.
277      */
278     private int currentArticleNumber = -1;
279 
280     /***
281      * Per-service configuration data that applies to all handlers
282      * associated with the service.
283      */
284     private NNTPHandlerConfigurationData theConfigData;
285 
286     /***
287      * The user id associated with the NNTP dialogue
288      */
289     private String user = null;
290 
291     /***
292      * The password associated with the NNTP dialogue
293      */
294     private String password = null;
295 
296     /***
297      * Whether the user for this session has already authenticated.
298      * Used to optimize authentication checks
299      */
300     boolean isAlreadyAuthenticated = false;
301 
302     /***
303      * The watchdog being used by this handler to deal with idle timeouts.
304      */
305     private Watchdog theWatchdog;
306 
307     /***
308      * The watchdog target that idles out this handler.
309      */
310     private WatchdogTarget theWatchdogTarget = new NNTPWatchdogTarget();
311 
312     /***
313      * Set the configuration data for the handler
314      *
315      * @param theData configuration data for the handler
316      */
317     void setConfigurationData(NNTPHandlerConfigurationData theData) {
318         theConfigData = theData;
319     }
320 
321     /***
322      * Set the Watchdog for use by this handler.
323      *
324      * @param theWatchdog the watchdog
325      */
326     void setWatchdog(Watchdog theWatchdog) {
327         this.theWatchdog = theWatchdog;
328     }
329 
330     /***
331      * Gets the Watchdog Target that should be used by Watchdogs managing
332      * this connection.
333      *
334      * @return the WatchdogTarget
335      */
336     WatchdogTarget getWatchdogTarget() {
337         return theWatchdogTarget;
338     }
339 
340     /***
341      * Idle out this connection
342      */
343     void idleClose() {
344         if (getLogger() != null) {
345             getLogger().error("NNTP Connection has idled out.");
346         }
347         try {
348             if (socket != null) {
349                 socket.close();
350             }
351         } catch (Exception e) {
352             // ignored
353         } finally {
354             socket = null;
355         }
356 
357         synchronized (this) {
358             // Interrupt the thread to recover from internal hangs
359             if (handlerThread != null) {
360                 handlerThread.interrupt();
361                 handlerThread = null;
362             }
363         }
364     }
365 
366     /***
367      * @see org.apache.avalon.cornerstone.services.connection.ConnectionHandler#handleConnection(Socket)
368      */
369     public void handleConnection( Socket connection ) throws IOException {
370         try {
371             this.socket = connection;
372             synchronized (this) {
373                 handlerThread = Thread.currentThread();
374             }
375             remoteIP = socket.getInetAddress().getHostAddress();
376             remoteHost = socket.getInetAddress().getHostName();
377             in = new BufferedInputStream(socket.getInputStream(), 1024);
378             // An ASCII encoding can be used because all transmissions other
379             // that those in the message body command are guaranteed
380             // to be ASCII
381             reader = new BufferedReader(new InputStreamReader(in, "ASCII"), 512);
382             outs = new BufferedOutputStream(socket.getOutputStream(), 1024);
383             writer = new InternetPrintWriter(outs, true);
384         } catch (Exception e) {
385             StringBuffer exceptionBuffer = 
386                 new StringBuffer(256)
387                     .append("Cannot open connection from ")
388                     .append(remoteHost)
389                     .append(" (")
390                     .append(remoteIP)
391                     .append("): ")
392                     .append(e.getMessage());
393             String exceptionString = exceptionBuffer.toString();
394             getLogger().error(exceptionString, e );
395         }
396 
397         try {
398             // section 7.1
399             if ( theConfigData.getNNTPRepository().isReadOnly() ) {
400                 StringBuffer respBuffer =
401                     new StringBuffer(128)
402                         .append("201 ")
403                         .append(theConfigData.getHelloName())
404                         .append(" NNTP Service Ready, posting prohibited");
405                 writeLoggedFlushedResponse(respBuffer.toString());
406             } else {
407                 StringBuffer respBuffer =
408                     new StringBuffer(128)
409                             .append("200 ")
410                             .append(theConfigData.getHelloName())
411                             .append(" NNTP Service Ready, posting permitted");
412                 writeLoggedFlushedResponse(respBuffer.toString());
413             }
414 
415             theWatchdog.start();
416             while (parseCommand(reader.readLine())) {
417                 theWatchdog.reset();
418             }
419             theWatchdog.stop();
420 
421             getLogger().info("Connection closed");
422         } catch (Exception e) {
423             // If the connection has been idled out, the
424             // socket will be closed and null.  Do NOT
425             // log the exception or attempt to send the
426             // closing connection message
427             if (socket != null) {
428                 try {
429                     doQUIT(null);
430                 } catch (Throwable t) {}
431                 getLogger().error( "Exception during connection:" + e.getMessage(), e );
432             }
433         } finally {
434             resetHandler();
435         }
436     }
437 
438     /***
439      * Resets the handler data to a basic state.
440      */
441     private void resetHandler() {
442 
443         // Clear the Watchdog
444         if (theWatchdog != null) {
445             ContainerUtil.dispose(theWatchdog);
446             theWatchdog = null;
447         }
448 
449         // Clear the streams
450         try {
451             if (reader != null) {
452                 reader.close();
453             }
454         } catch (IOException ioe) {
455             getLogger().warn("NNTPHandler: Unexpected exception occurred while closing reader: " + ioe);
456         } finally {
457             reader = null;
458         }
459 
460         in = null;
461 
462         if (writer != null) {
463             writer.close();
464             writer = null;
465         }
466         outs = null;
467 
468         remoteHost = null;
469         remoteIP = null;
470         try {
471             if (socket != null) {
472                 socket.close();
473             }
474         } catch (IOException ioe) {
475             getLogger().warn("NNTPHandler: Unexpected exception occurred while closing socket: " + ioe);
476         } finally {
477             socket = null;
478         }
479 
480         synchronized (this) {
481             handlerThread = null;
482         }
483 
484         // Clear the selected group, article info
485         group = null;
486         currentArticleNumber = -1;
487 
488         // Clear the authentication info
489         user = null;
490         password = null;
491         isAlreadyAuthenticated = false;
492 
493         // Clear the config data
494         theConfigData = null;
495     }
496 
497     /***
498      * This method parses NNTP commands read off the wire in handleConnection.
499      * Actual processing of the command (possibly including additional back and
500      * forth communication with the client) is delegated to one of a number of
501      * command specific handler methods.  The primary purpose of this method is
502      * to parse the raw command string to determine exactly which handler should
503      * be called.  It returns true if expecting additional commands, false otherwise.
504      *
505      * @param commandRaw the raw command string passed in over the socket
506      *
507      * @return whether additional commands are expected.
508      */
509     private boolean parseCommand(String commandRaw) {
510         if (commandRaw == null) {
511             return false;
512         }
513         if (getLogger().isDebugEnabled()) {
514             getLogger().debug("Command received: " + commandRaw);
515         }
516 
517         String command = commandRaw.trim();
518         String argument = null;
519         int spaceIndex = command.indexOf(" ");
520         if (spaceIndex >= 0) {
521             argument = command.substring(spaceIndex + 1);
522             command = command.substring(0, spaceIndex);
523         }
524         command = command.toUpperCase(Locale.US);
525 
526         boolean returnValue = true;
527         if (!isAuthorized(command) ) {
528             writeLoggedFlushedResponse("480 User is not authenticated");
529             getLogger().debug("Command not allowed.");
530             return returnValue;
531         }
532         if ((command.equals(COMMAND_MODE)) && (argument != null)) {
533             if (argument.toUpperCase(Locale.US).equals(MODE_TYPE_READER)) {
534                 doMODEREADER(argument);
535             } else if (argument.toUpperCase(Locale.US).equals(MODE_TYPE_STREAM)) {
536                 doMODESTREAM(argument);
537             } else {
538                 writeLoggedFlushedResponse("500 Command not understood");
539             }
540         } else if ( command.equals(COMMAND_LIST)) {
541             doLIST(argument);
542         } else if ( command.equals(COMMAND_GROUP) ) {
543             doGROUP(argument);
544         } else if ( command.equals(COMMAND_NEXT) ) {
545             doNEXT(argument);
546         } else if ( command.equals(COMMAND_LAST) ) {
547             doLAST(argument);
548         } else if ( command.equals(COMMAND_ARTICLE) ) {
549             doARTICLE(argument);
550         } else if ( command.equals(COMMAND_HEAD) ) {
551             doHEAD(argument);
552         } else if ( command.equals(COMMAND_BODY) ) {
553             doBODY(argument);
554         } else if ( command.equals(COMMAND_STAT) ) {
555             doSTAT(argument);
556         } else if ( command.equals(COMMAND_POST) ) {
557             doPOST(argument);
558         } else if ( command.equals(COMMAND_IHAVE) ) {
559             doIHAVE(argument);
560         } else if ( command.equals(COMMAND_QUIT) ) {
561             doQUIT(argument);
562             returnValue = false;
563         } else if ( command.equals(COMMAND_DATE) ) {
564             doDATE(argument);
565         } else if ( command.equals(COMMAND_HELP) ) {
566             doHELP(argument);
567         } else if ( command.equals(COMMAND_NEWGROUPS) ) {
568             doNEWGROUPS(argument);
569         } else if ( command.equals(COMMAND_NEWNEWS) ) {
570             doNEWNEWS(argument);
571         } else if ( command.equals(COMMAND_LISTGROUP) ) {
572             doLISTGROUP(argument);
573         } else if ( command.equals(COMMAND_OVER) ) {
574             doOVER(argument);
575         } else if ( command.equals(COMMAND_XOVER) ) {
576             doXOVER(argument);
577         } else if ( command.equals(COMMAND_HDR) ) {
578             doHDR(argument);
579         } else if ( command.equals(COMMAND_XHDR) ) {
580             doXHDR(argument);
581         } else if ( command.equals(COMMAND_AUTHINFO) ) {
582             doAUTHINFO(argument);
583         } else if ( command.equals(COMMAND_SLAVE) ) {
584             doSLAVE(argument);
585         } else if ( command.equals(COMMAND_PAT) ) {
586             doPAT(argument);
587         } else {
588             doUnknownCommand(command, argument);
589         }
590         return returnValue;
591     }
592 
593     /***
594      * Handles an unrecognized command, logging that.
595      *
596      * @param command the command received from the client
597      * @param argument the argument passed in with the command
598      */
599     private void doUnknownCommand(String command, String argument) {
600         if (getLogger().isDebugEnabled()) {
601             StringBuffer logBuffer =
602                 new StringBuffer(128)
603                     .append("Received unknown command ")
604                     .append(command)
605                     .append(" with argument ")
606                     .append(argument);
607             getLogger().debug(logBuffer.toString());
608         }
609         writeLoggedFlushedResponse("500 Unknown command");
610     }
611 
612     /***
613      * Implements only the originnal AUTHINFO.
614      * for simple and generic AUTHINFO, 501 is sent back. This is as
615      * per article 3.1.3 of RFC 2980
616      *
617      * @param argument the argument passed in with the AUTHINFO command
618      */
619     private void doAUTHINFO(String argument) {
620         String command = null;
621         String value = null;
622         if (argument != null) {
623             int spaceIndex = argument.indexOf(" ");
624             if (spaceIndex >= 0) {
625                 command = argument.substring(0, spaceIndex);
626                 value = argument.substring(spaceIndex + 1);
627             }
628         }
629         if (command == null) {
630             writeLoggedFlushedResponse("501 Syntax error");
631             return;
632         }
633         command = command.toUpperCase(Locale.US);
634         if ( command.equals(AUTHINFO_PARAM_USER) ) {
635             // Reject re-authentication
636             if ( isAlreadyAuthenticated ) {
637                 writeLoggedFlushedResponse("482 Already authenticated - rejecting new credentials");
638             }
639             // Reject doubly sent user
640             if (user != null) {
641                 user = null;
642                 password = null;
643                 isAlreadyAuthenticated = false;
644                 writeLoggedFlushedResponse("482 User already specified - rejecting new user");
645                 return;
646             }
647             user = value;
648             writeLoggedFlushedResponse("381 More authentication information required");
649         } else if ( command.equals(AUTHINFO_PARAM_PASS) ) {
650             // Reject password sent before user
651             if (user == null) {
652                 writeLoggedFlushedResponse("482 User not yet specified.  Rejecting user.");
653                 return;
654             }
655             // Reject doubly sent password
656             if (password != null) {
657                 user = null;
658                 password = null;
659                 isAlreadyAuthenticated = false;
660                 writeLoggedFlushedResponse("482 Password already specified - rejecting new password");
661                 return;
662             }
663             password = value;
664             isAlreadyAuthenticated = isAuthenticated();
665             if ( isAlreadyAuthenticated ) {
666                 writeLoggedFlushedResponse("281 Authentication accepted");
667             } else {
668                 writeLoggedFlushedResponse("482 Authentication rejected");
669                 // Clear bad authentication
670                 user = null;
671                 password = null;
672             }
673         } else {
674             writeLoggedFlushedResponse("501 Syntax error");
675             return;
676         }
677     }
678 
679     /***
680      * Lists the articles posted since the date passed in as
681      * an argument.
682      *
683      * @param argument the argument passed in with the NEWNEWS command.
684      *                 Should be a wildmat followed by a date.
685      */
686     private void doNEWNEWS(String argument) {
687         // see section 11.4
688 
689         String wildmat = "*";
690 
691         if (argument != null) {
692             int spaceIndex = argument.indexOf(" ");
693             if (spaceIndex >= 0) {
694                 wildmat = argument.substring(0, spaceIndex);
695                 argument = argument.substring(spaceIndex + 1);
696             } else {
697                 getLogger().error("NEWNEWS had an invalid argument");
698                 writeLoggedFlushedResponse("501 Syntax error");
699                 return;
700             }
701         } else {
702             getLogger().error("NEWNEWS had a null argument");
703             writeLoggedFlushedResponse("501 Syntax error");
704             return;
705         }
706 
707         Date theDate = null;
708         try {
709             theDate = getDateFrom(argument);
710         } catch (NNTPException nntpe) {
711             getLogger().error("NEWNEWS had an invalid argument", nntpe);
712             writeLoggedFlushedResponse("501 Syntax error");
713             return;
714         }
715 
716         writeLoggedFlushedResponse("230 list of new articles by message-id follows");
717         Iterator groupIter = theConfigData.getNNTPRepository().getMatchedGroups(wildmat);
718         while ( groupIter.hasNext() ) {
719             Iterator articleIter = ((NNTPGroup)(groupIter.next())).getArticlesSince(theDate);
720             while (articleIter.hasNext()) {
721                 StringBuffer iterBuffer =
722                     new StringBuffer(64)
723                         .append(((NNTPArticle)articleIter.next()).getUniqueID());
724                 writeLoggedResponse(iterBuffer.toString());
725             }
726         }
727         writeLoggedFlushedResponse(".");
728     }
729 
730     /***
731      * Lists the groups added since the date passed in as
732      * an argument.
733      *
734      * @param argument the argument passed in with the NEWGROUPS command.
735      *                 Should be a date.
736      */
737     private void doNEWGROUPS(String argument) {
738         // see section 11.3
739         // both draft-ietf-nntpext-base-15.txt and rfc977 have only group names 
740         // in response lines, but INN sends 
741         // '<group name> <last article> <first article> <posting allowed>'
742         // NOTE: following INN over either document.
743         //
744         // TODO: Check this.  Audit at http://www.academ.com/pipermail/ietf-nntp/2001-July/002185.html
745         // doesn't mention the supposed discrepancy.  Consider changing code to 
746         // be in line with spec.
747         Date theDate = null;
748         try {
749             theDate = getDateFrom(argument);
750         } catch (NNTPException nntpe) {
751             getLogger().error("NEWGROUPS had an invalid argument", nntpe);
752             writeLoggedFlushedResponse("501 Syntax error");
753             return;
754         }
755         Iterator iter = theConfigData.getNNTPRepository().getGroupsSince(theDate);
756         writeLoggedFlushedResponse("231 list of new newsgroups follows");
757         while ( iter.hasNext() ) {
758             NNTPGroup currentGroup = (NNTPGroup)iter.next();
759             StringBuffer iterBuffer =
760                 new StringBuffer(128)
761                     .append(currentGroup.getName())
762                     .append(" ")
763                     .append(currentGroup.getLastArticleNumber())
764                     .append(" ")
765                     .append(currentGroup.getFirstArticleNumber())
766                     .append(" ")
767                     .append((currentGroup.isPostAllowed()?"y":"n"));
768             writeLoggedResponse(iterBuffer.toString());
769         }
770         writeLoggedFlushedResponse(".");
771     }
772 
773     /***
774      * Lists the help text for the service.
775      *
776      * @param argument the argument passed in with the HELP command.
777      */
778     private void doHELP(String argument) {
779         writeLoggedResponse("100 Help text follows");
780         writeLoggedFlushedResponse(".");
781     }
782 
783     /***
784      * Acknowledges a SLAVE command.  No special preference is given
785      * to slave connections.
786      *
787      * @param argument the argument passed in with the SLAVE command.
788      */
789     private void doSLAVE(String argument) {
790         writeLoggedFlushedResponse("202 slave status noted");
791     }
792 
793     /***
794      * Returns the current date according to the news server.
795      *
796      * @param argument the argument passed in with the DATE command
797      */
798     private void doDATE(String argument) {
799         Date dt = new Date(System.currentTimeMillis()-UTC_OFFSET);
800         String dtStr = DF_RFC2980.format(new Date(dt.getTime() - UTC_OFFSET));
801         writeLoggedFlushedResponse("111 " + dtStr);
802     }
803 
804     /***
805      * Quits the transaction.
806      *
807      * @param argument the argument passed in with the QUIT command
808      */
809     private void doQUIT(String argument) {
810         writeLoggedFlushedResponse("205 closing connection");
811     }
812 
813     /***
814      * Handles the LIST command and its assorted extensions.
815      *
816      * @param argument the argument passed in with the LIST command.
817      */
818     private void doLIST(String argument) {
819         // see section 9.4.1
820         String wildmat = "*";
821         boolean isListNewsgroups = false;
822 
823         String extension = argument;
824         if (argument != null) {
825             int spaceIndex = argument.indexOf(" ");
826             if (spaceIndex >= 0) {
827                 wildmat = argument.substring(spaceIndex + 1);
828                 extension = argument.substring(0, spaceIndex);
829             }
830             extension = extension.toUpperCase(Locale.US);
831         }
832 
833         if (extension != null) {
834             if (extension.equals("ACTIVE")) {
835                 isListNewsgroups = false;
836             } else if (extension.equals("NEWSGROUPS") ) {
837                 isListNewsgroups = true;
838             } else if (extension.equals("EXTENSIONS") ) {
839                 doLISTEXTENSIONS();
840                 return;
841             } else if (extension.equals("OVERVIEW.FMT") ) {
842                 doLISTOVERVIEWFMT();
843                 return;
844             } else if (extension.equals("ACTIVE.TIMES") ) {
845                 // not supported - 9.4.2.1, 9.4.3.1, 9.4.4.1
846                 writeLoggedFlushedResponse("503 program error, function not performed");
847                 return;
848             } else if (extension.equals("DISTRIBUTIONS") ) {
849                 // not supported - 9.4.2.1, 9.4.3.1, 9.4.4.1
850                 writeLoggedFlushedResponse("503 program error, function not performed");
851                 return;
852             } else if (extension.equals("DISTRIB.PATS") ) {
853                 // not supported - 9.4.2.1, 9.4.3.1, 9.4.4.1
854                 writeLoggedFlushedResponse("503 program error, function not performed");
855                 return;
856             } else {
857                 writeLoggedFlushedResponse("501 Syntax error");
858                 return;
859             }
860         }
861 
862         Iterator iter = theConfigData.getNNTPRepository().getMatchedGroups(wildmat);
863         writeLoggedFlushedResponse("215 list of newsgroups follows");
864         while ( iter.hasNext() ) {
865             NNTPGroup theGroup = (NNTPGroup)iter.next();
866             if (isListNewsgroups) {
867                 writeLoggedResponse(theGroup.getListNewsgroupsFormat());
868             } else {
869                 writeLoggedResponse(theGroup.getListFormat());
870             }
871         }
872         writeLoggedFlushedResponse(".");
873     }
874 
875     /***
876      * Informs the server that the client has an article with the specified
877      * message-ID.
878      *
879      * @param id the message id
880      */
881     private void doIHAVE(String id) {
882         // see section 9.3.2.1
883         if (!isMessageId(id)) {
884             writeLoggedFlushedResponse("501 command syntax error");
885             return;
886         }
887         NNTPArticle article = theConfigData.getNNTPRepository().getArticleFromID(id);
888         if ( article != null ) {
889             writeLoggedFlushedResponse("435 article not wanted - do not send it");
890         } else {
891             writeLoggedFlushedResponse("335 send article to be transferred. End with <CR-LF>.<CR-LF>");
892             try {
893                 createArticle();
894             } catch (RuntimeException e) {
895                 writeLoggedFlushedResponse("436 transfer failed - try again later");
896                 throw e;
897             }
898             writeLoggedFlushedResponse("235 article received ok");
899         }
900     }
901 
902     /***
903      * Posts an article to the news server.
904      *
905      * @param argument the argument passed in with the POST command
906      */
907     private void doPOST(String argument) {
908         // see section 9.3.1.1
909         if ( argument != null ) {
910             writeLoggedFlushedResponse("501 Syntax error - unexpected parameter");
911         }
912         writeLoggedFlushedResponse("340 send article to be posted. End with <CR-LF>.<CR-LF>");
913         createArticle();
914         writeLoggedFlushedResponse("240 article received ok");
915     }
916 
917     /***
918      * Executes the STAT command.  Sets the current article pointer,
919      * returns article information.
920      *
921      * @param the argument passed in to the STAT command,
922      *        which should be an article number or message id.
923      *        If no parameter is provided, the current selected
924      *        article is used.
925      */
926     private void doSTAT(String param) {
927         // section 9.2.4
928         NNTPArticle article = null;
929         if (isMessageId(param)) {
930             article = theConfigData.getNNTPRepository().getArticleFromID(param);
931             if ( article == null ) {
932                 writeLoggedFlushedResponse("430 no such article");
933                 return;
934             } else {
935                 StringBuffer respBuffer =
936                     new StringBuffer(64)
937                             .append("223 0 ")
938                             .append(param);
939                 writeLoggedFlushedResponse(respBuffer.toString());
940             }
941         } else {
942             int newArticleNumber = currentArticleNumber;
943             if ( group == null ) {
944                 writeLoggedFlushedResponse("412 no newsgroup selected");
945                 return;
946             } else {
947                 if ( param == null ) {
948                     if ( currentArticleNumber < 0 ) {
949                         writeLoggedFlushedResponse("420 no current article selected");
950                         return;
951                     } else {
952                         article = group.getArticle(currentArticleNumber);
953                     }
954                 }
955                 else {
956                     newArticleNumber = Integer.parseInt(param);
957                     article = group.getArticle(newArticleNumber);
958                 }
959                 if ( article == null ) {
960                     writeLoggedFlushedResponse("423 no such article number in this group");
961                     return;
962                 } else {
963                     currentArticleNumber = newArticleNumber;
964                     String articleID = article.getUniqueID();
965                     if (articleID == null) {
966                         articleID = "<0>";
967                     }
968                     StringBuffer respBuffer =
969                         new StringBuffer(128)
970                                 .append("223 ")
971                                 .append(article.getArticleNumber())
972                                 .append(" ")
973                                 .append(articleID);
974                     writeLoggedFlushedResponse(respBuffer.toString());
975                 }
976             }
977         }
978     }
979 
980     /***
981      * Executes the BODY command.  Sets the current article pointer,
982      * returns article information and body.
983      *
984      * @param the argument passed in to the BODY command,
985      *        which should be an article number or message id.
986      *        If no parameter is provided, the current selected
987      *        article is used.
988      */
989     private void doBODY(String param) {
990         // section 9.2.3
991         NNTPArticle article = null;
992         if (isMessageId(param)) {
993             article = theConfigData.getNNTPRepository().getArticleFromID(param);
994             if ( article == null ) {
995                 writeLoggedFlushedResponse("430 no such article");
996                 return;
997             } else {
998                 StringBuffer respBuffer =
999                     new StringBuffer(64)
1000                             .append("222 0 ")
1001                             .append(param);
1002                 writeLoggedFlushedResponse(respBuffer.toString());
1003             }
1004         } else {
1005             int newArticleNumber = currentArticleNumber;
1006             if ( group == null ) {
1007                 writeLoggedFlushedResponse("412 no newsgroup selected");
1008                 return;
1009             } else {
1010                 if ( param == null ) {
1011                     if ( currentArticleNumber < 0 ) {
1012                         writeLoggedFlushedResponse("420 no current article selected");
1013                         return;
1014                     } else {
1015                         article = group.getArticle(currentArticleNumber);
1016                     }
1017                 }
1018                 else {
1019                     newArticleNumber = Integer.parseInt(param);
1020                     article = group.getArticle(newArticleNumber);
1021                 }
1022                 if ( article == null ) {
1023                     writeLoggedFlushedResponse("423 no such article number in this group");
1024                     return;
1025                 } else {
1026                     currentArticleNumber = newArticleNumber;
1027                     String articleID = article.getUniqueID();
1028                     if (articleID == null) {
1029                         articleID = "<0>";
1030                     }
1031                     StringBuffer respBuffer =
1032                         new StringBuffer(128)
1033                                 .append("222 ")
1034                                 .append(article.getArticleNumber())
1035                                 .append(" ")
1036                                 .append(articleID);
1037                     writeLoggedFlushedResponse(respBuffer.toString());
1038                 }
1039             }
1040         }
1041         if (article != null) {
1042             writer.flush();
1043             article.writeBody(new ExtraDotOutputStream(outs));
1044             writeLoggedFlushedResponse("\r\n.");
1045         }
1046     }
1047 
1048     /***
1049      * Executes the HEAD command.  Sets the current article pointer,
1050      * returns article information and headers.
1051      *
1052      * @param the argument passed in to the HEAD command,
1053      *        which should be an article number or message id.
1054      *        If no parameter is provided, the current selected
1055      *        article is used.
1056      */
1057     private void doHEAD(String param) {
1058         // section 9.2.2
1059         NNTPArticle article = null;
1060         if (isMessageId(param)) {
1061             article = theConfigData.getNNTPRepository().getArticleFromID(param);
1062             if ( article == null ) {
1063                 writeLoggedFlushedResponse("430 no such article");
1064                 return;
1065             } else {
1066                 StringBuffer respBuffer =
1067                     new StringBuffer(64)
1068                             .append("221 0 ")
1069                             .append(param);
1070                 writeLoggedFlushedResponse(respBuffer.toString());
1071             }
1072         } else {
1073             int newArticleNumber = currentArticleNumber;
1074             if ( group == null ) {
1075                 writeLoggedFlushedResponse("412 no newsgroup selected");
1076                 return;
1077             } else {
1078                 if ( param == null ) {
1079                     if ( currentArticleNumber < 0 ) {
1080                         writeLoggedFlushedResponse("420 no current article selected");
1081                         return;
1082                     } else {
1083                         article = group.getArticle(currentArticleNumber);
1084                     }
1085                 }
1086                 else {
1087                     newArticleNumber = Integer.parseInt(param);
1088                     article = group.getArticle(newArticleNumber);
1089                 }
1090                 if ( article == null ) {
1091                     writeLoggedFlushedResponse("423 no such article number in this group");
1092                     return;
1093                 } else {
1094                     currentArticleNumber = newArticleNumber;
1095                     String articleID = article.getUniqueID();
1096                     if (articleID == null) {
1097                         articleID = "<0>";
1098                     }
1099                     StringBuffer respBuffer =
1100                         new StringBuffer(128)
1101                                 .append("221 ")
1102                                 .append(article.getArticleNumber())
1103                                 .append(" ")
1104                                 .append(articleID);
1105                     writeLoggedFlushedResponse(respBuffer.toString());
1106                 }
1107             }
1108         }
1109         if (article != null) {
1110             writer.flush();
1111             article.writeHead(new ExtraDotOutputStream(outs));
1112             writeLoggedFlushedResponse(".");
1113         }
1114     }
1115 
1116     /***
1117      * Executes the ARTICLE command.  Sets the current article pointer,
1118      * returns article information and contents.
1119      *
1120      * @param the argument passed in to the ARTICLE command,
1121      *        which should be an article number or message id.
1122      *        If no parameter is provided, the current selected
1123      *        article is used.
1124      */
1125     private void doARTICLE(String param) {
1126         // section 9.2.1
1127         NNTPArticle article = null;
1128         if (isMessageId(param)) {
1129             article = theConfigData.getNNTPRepository().getArticleFromID(param);
1130             if ( article == null ) {
1131                 writeLoggedFlushedResponse("430 no such article");
1132                 return;
1133             } else {
1134                 StringBuffer respBuffer =
1135                     new StringBuffer(64)
1136                             .append("220 0 ")
1137                             .append(param);
1138                 writeLoggedResponse(respBuffer.toString());
1139             }
1140         } else {
1141             int newArticleNumber = currentArticleNumber;
1142             if ( group == null ) {
1143                 writeLoggedFlushedResponse("412 no newsgroup selected");
1144                 return;
1145             } else {
1146                 if ( param == null ) {
1147                     if ( currentArticleNumber < 0 ) {
1148                         writeLoggedFlushedResponse("420 no current article selected");
1149                         return;
1150                     } else {
1151                         article = group.getArticle(currentArticleNumber);
1152                     }
1153                 }
1154                 else {
1155                     newArticleNumber = Integer.parseInt(param);
1156                     article = group.getArticle(newArticleNumber);
1157                 }
1158                 if ( article == null ) {
1159                     writeLoggedFlushedResponse("423 no such article number in this group");
1160                     return;
1161                 } else {
1162                     currentArticleNumber = newArticleNumber;
1163                     String articleID = article.getUniqueID();
1164                     if (articleID == null) {
1165                         articleID = "<0>";
1166                     }
1167                     StringBuffer respBuffer =
1168                         new StringBuffer(128)
1169                                 .append("220 ")
1170                                 .append(article.getArticleNumber())
1171                                 .append(" ")
1172                                 .append(articleID);
1173                     writeLoggedFlushedResponse(respBuffer.toString());
1174                 }
1175             }
1176         }
1177         if (article != null) {
1178             writer.flush();
1179             article.writeArticle(new ExtraDotOutputStream(outs));
1180             // see jira JAMES-311 for an explanation of the "\r\n"
1181             writeLoggedFlushedResponse("\r\n.");
1182         }
1183     }
1184 
1185     /***
1186      * Advances the current article pointer to the next article in the group.
1187      *
1188      * @param argument the argument passed in with the NEXT command
1189      */
1190     private void doNEXT(String argument) {
1191         // section 9.1.1.3.1
1192         if ( argument != null ) {
1193             writeLoggedFlushedResponse("501 Syntax error - unexpected parameter");
1194         } else if ( group == null ) {
1195             writeLoggedFlushedResponse("412 no newsgroup selected");
1196         } else if ( currentArticleNumber < 0 ) {
1197             writeLoggedFlushedResponse("420 no current article has been selected");
1198         } else if ( currentArticleNumber >= group.getLastArticleNumber() ) {
1199             writeLoggedFlushedResponse("421 no next article in this group");
1200         } else {
1201             currentArticleNumber++;
1202             NNTPArticle article = group.getArticle(currentArticleNumber);
1203             StringBuffer respBuffer =
1204                 new StringBuffer(64)
1205                         .append("223 ")
1206                         .append(article.getArticleNumber())
1207                         .append(" ")
1208                         .append(article.getUniqueID());
1209             writeLoggedFlushedResponse(respBuffer.toString());
1210         }
1211     }
1212 
1213     /***
1214      * Moves the currently selected article pointer to the article
1215      * previous to the currently selected article in the selected group.
1216      *
1217      * @param argument the argument passed in with the LAST command
1218      */
1219     private void doLAST(String argument) {
1220         // section 9.1.1.2.1
1221         if ( argument != null ) {
1222             writeLoggedFlushedResponse("501 Syntax error - unexpected parameter");
1223         } else if ( group == null ) {
1224             writeLoggedFlushedResponse("412 no newsgroup selected");
1225         } else if ( currentArticleNumber < 0 ) {
1226             writeLoggedFlushedResponse("420 no current article has been selected");
1227         } else if ( currentArticleNumber <= group.getFirstArticleNumber() ) {
1228             writeLoggedFlushedResponse("422 no previous article in this group");
1229         } else {
1230             currentArticleNumber--;
1231             NNTPArticle article = group.getArticle(currentArticleNumber);
1232             StringBuffer respBuffer =
1233                 new StringBuffer(64)
1234                         .append("223 ")
1235                         .append(article.getArticleNumber())
1236                         .append(" ")
1237                         .append(article.getUniqueID());
1238             writeLoggedFlushedResponse(respBuffer.toString());
1239         }
1240     }
1241 
1242     /***
1243      * Selects a group to be the current newsgroup.
1244      *
1245      * @param group the name of the group being selected.
1246      */
1247     private void doGROUP(String groupName) {
1248         if (groupName == null) {
1249             writeLoggedFlushedResponse("501 Syntax error - missing required parameter");
1250             return;
1251         }
1252         NNTPGroup newGroup = theConfigData.getNNTPRepository().getGroup(groupName);
1253         // section 9.1.1.1
1254         if ( newGroup == null ) {
1255             writeLoggedFlushedResponse("411 no such newsgroup");
1256         } else {
1257             group = newGroup;
1258             // if the number of articles in group == 0
1259             // then the server may return this information in 3 ways, 
1260             // The clients must honor all those 3 ways.
1261             // our response is: 
1262             // highWaterMark == lowWaterMark and number of articles == 0
1263             int articleCount = group.getNumberOfArticles();
1264             int lowWaterMark = group.getFirstArticleNumber();
1265             int highWaterMark = group.getLastArticleNumber();
1266 
1267             // Set the current article pointer.  If no
1268             // articles are in the group, the current article
1269             // pointer should be explicitly unset.
1270             if (articleCount != 0) {
1271                 currentArticleNumber = lowWaterMark;
1272             } else {
1273                 currentArticleNumber = -1;
1274             }
1275             StringBuffer respBuffer =
1276                 new StringBuffer(128)
1277                         .append("211 ")
1278                         .append(articleCount)
1279                         .append(" ")
1280                         .append(lowWaterMark)
1281                         .append(" ")
1282                         .append(highWaterMark)
1283                         .append(" ")
1284                         .append(group.getName())
1285                         .append(" group selected");
1286             writeLoggedFlushedResponse(respBuffer.toString());
1287         }
1288     }
1289 
1290     /***
1291      * Lists the extensions supported by this news server.
1292      */
1293     private void doLISTEXTENSIONS() {
1294         // 8.1.1
1295         writeLoggedResponse("202 Extensions supported:");
1296         writeLoggedResponse("LISTGROUP");
1297         writeLoggedResponse("AUTHINFO");
1298         writeLoggedResponse("OVER");
1299         writeLoggedResponse("XOVER");
1300         writeLoggedResponse("HDR");
1301         writeLoggedResponse("XHDR");
1302         writeLoggedFlushedResponse(".");
1303     }
1304 
1305     /***
1306      * Informs the server that the client is a newsreader.
1307      *
1308      * @param argument the argument passed in with the MODE READER command
1309      */
1310     private void doMODEREADER(String argument) {
1311         // 7.2
1312         writeLoggedFlushedResponse(theConfigData.getNNTPRepository().isReadOnly()
1313                        ? "201 Posting Not Permitted" : "200 Posting Permitted");
1314     }
1315 
1316     /***
1317      * Informs the server that the client is a news server.
1318      *
1319      * @param argument the argument passed in with the MODE STREAM command
1320      */
1321     private void doMODESTREAM(String argument) {
1322         // 7.2
1323         writeLoggedFlushedResponse("500 Command not understood");
1324     }
1325 
1326     /***
1327      * Gets a listing of article numbers in specified group name
1328      * or in the already selected group if the groupName is null.
1329      *
1330      * @param groupName the name of the group to list
1331      */
1332     private void doLISTGROUP(String groupName) {
1333         // 9.5.1.1.1
1334         if (groupName==null) {
1335             if ( group == null) {
1336                 writeLoggedFlushedResponse("412 no news group currently selected");
1337                 return;
1338             }
1339         }
1340         else {
1341             group = theConfigData.getNNTPRepository().getGroup(groupName);
1342             if ( group == null ) {
1343                 writeLoggedFlushedResponse("411 no such newsgroup");
1344                 return;
1345             }
1346         }
1347         if ( group != null ) {
1348             // this.group = group;
1349 
1350             // Set the current article pointer.  If no
1351             // articles are in the group, the current article
1352             // pointer should be explicitly unset.
1353             if (group.getNumberOfArticles() > 0) {
1354                 currentArticleNumber = group.getFirstArticleNumber();
1355             } else {
1356                 currentArticleNumber = -1;
1357             }
1358 
1359             writeLoggedFlushedResponse("211 list of article numbers follow");
1360 
1361             Iterator iter = group.getArticles();
1362             while (iter.hasNext()) {
1363                 NNTPArticle article = (NNTPArticle)iter.next();
1364                 writeLoggedResponse(article.getArticleNumber() + "");
1365             }
1366             writeLoggedFlushedResponse(".");
1367         }
1368     }
1369 
1370     /***
1371      * Handles the LIST OVERVIEW.FMT command.  Not supported.
1372      */
1373     private void doLISTOVERVIEWFMT() {
1374         // 9.5.3.1.1
1375         writeLoggedFlushedResponse("215 Information follows");
1376         String[] overviewHeaders = theConfigData.getNNTPRepository().getOverviewFormat();
1377         for (int i = 0;  i < overviewHeaders.length; i++) {
1378             writeLoggedResponse(overviewHeaders[i]);
1379         }
1380         writeLoggedFlushedResponse(".");
1381     }
1382 
1383     /***
1384      * Handles the PAT command.  Not supported.
1385      *
1386      * @param argument the argument passed in with the PAT command
1387      */
1388     private void doPAT(String argument) {
1389         // 9.5.3.1.1 in draft-12
1390         writeLoggedFlushedResponse("500 Command not recognized");
1391     }
1392 
1393     /***
1394      * Get the values of the headers for the selected newsgroup, 
1395      * with an optional range modifier.
1396      *
1397      * @param argument the argument passed in with the XHDR command.
1398      */
1399     private void doXHDR(String argument) {
1400         doHDR(argument);
1401     }
1402 
1403     /***
1404      * Get the values of the headers for the selected newsgroup, 
1405      * with an optional range modifier.
1406      *
1407      * @param argument the argument passed in with the HDR command.
1408      */
1409     private void doHDR(String argument) {
1410         // 9.5.3
1411         if (argument == null) {
1412             writeLoggedFlushedResponse("501 Syntax error - missing required parameter");
1413             return;
1414         }
1415         String hdr = argument;
1416         String range = null;
1417         int spaceIndex = hdr.indexOf(" ");
1418         if (spaceIndex >= 0 ) {
1419             range = hdr.substring(spaceIndex + 1);
1420             hdr = hdr.substring(0, spaceIndex);
1421         }
1422         if (group == null ) {
1423             writeLoggedFlushedResponse("412 No news group currently selected.");
1424             return;
1425         }
1426         if ((range == null) && (currentArticleNumber < 0)) {
1427             writeLoggedFlushedResponse("420 No current article selected");
1428             return;
1429         }
1430         NNTPArticle[] article = getRange(range);
1431         if ( article == null ) {
1432             writeLoggedFlushedResponse("412 no newsgroup selected");
1433         } else if ( article.length == 0 ) {
1434             writeLoggedFlushedResponse("430 no such article");
1435         } else {
1436             writeLoggedFlushedResponse("221 Header follows");
1437             for ( int i = 0 ; i < article.length ; i++ ) {
1438                 String val = article[i].getHeader(hdr);
1439                 if ( val == null ) {
1440                     val = "";
1441                 }
1442                 StringBuffer hdrBuffer =
1443                     new StringBuffer(128)
1444                             .append(article[i].getArticleNumber())
1445                             .append(" ")
1446                             .append(val);
1447                 writeLoggedResponse(hdrBuffer.toString());
1448             }
1449             writeLoggedFlushedResponse(".");
1450         }
1451     }
1452 
1453     /***
1454      * Returns information from the overview database regarding the
1455      * current article, or a range of articles.
1456      *
1457      * @param range the optional article range.
1458      */
1459     private void doXOVER(String range) {
1460         doOVER(range);
1461     }
1462 
1463     /***
1464      * Returns information from the overview database regarding the
1465      * current article, or a range of articles.
1466      *
1467      * @param range the optional article range.
1468      */
1469     private void doOVER(String range) {
1470         // 9.5.2.2.1
1471         if ( group == null ) {
1472             writeLoggedFlushedResponse("412 No newsgroup selected");
1473             return;
1474         }
1475         if ((range == null) && (currentArticleNumber < 0)) {
1476             writeLoggedFlushedResponse("420 No current article selected");
1477             return;
1478         }
1479         NNTPArticle[] article = getRange(range);
1480         if ( article.length == 0 ) {
1481             writeLoggedFlushedResponse("420 No article(s) selected");
1482         } else {
1483             writeLoggedResponse("224 Overview information follows");
1484             for ( int i = 0 ; i < article.length ; i++ ) {
1485                 article[i].writeOverview(outs);
1486                 if (i % 100 == 0) {
1487                     // Reset the watchdog every hundred headers or so
1488                     // to ensure the connection doesn't timeout for slow
1489                     // clients
1490                     theWatchdog.reset();
1491                 }
1492             }
1493             writeLoggedFlushedResponse(".");
1494         }
1495     }
1496 
1497     /***
1498      * Handles the transaction for getting the article data.
1499      */
1500     private void createArticle() {
1501         try {
1502             InputStream msgIn = new CharTerminatedInputStream(in, NNTPTerminator);
1503             // Removes the dot stuffing
1504             msgIn = new DotStuffingInputStream(msgIn);
1505             MailHeaders headers = new MailHeaders(msgIn);
1506             processMessageHeaders(headers);
1507             processMessage(headers, msgIn);
1508         } catch (MessagingException me) {
1509             throw new NNTPException("MessagingException encountered when loading article.");
1510         }
1511     }
1512 
1513     /***
1514      * Processes the NNTP message headers coming in off the wire.
1515      *
1516      * @param headers the headers of the message being read
1517      */
1518     private MailHeaders processMessageHeaders(MailHeaders headers)
1519         throws MessagingException {
1520         return headers;
1521     }
1522 
1523     /***
1524      * Processes the NNTP message coming in off the wire.  Reads the
1525      * content and delivers to the spool.
1526      *
1527      * @param headers the headers of the message being read
1528      * @param msgIn the stream containing the message content
1529      */
1530     private void processMessage(MailHeaders headers, InputStream bodyIn)
1531         throws MessagingException {
1532         InputStream messageIn = null;
1533         try {
1534             messageIn = new SequenceInputStream(new ByteArrayInputStream(headers.toByteArray()), bodyIn);
1535             theConfigData.getNNTPRepository().createArticle(messageIn);
1536         } finally {
1537             if (messageIn != null) {
1538                 try {
1539                     messageIn.close();
1540                 } catch (IOException ioe) {
1541                     // Ignore exception on close.
1542                 }
1543                 messageIn = null;
1544             }
1545         }
1546     }
1547 
1548     /***
1549      * Returns the date from @param input.
1550      * The input tokens are assumed to be in format date time [GMT|UTC] .
1551      * 'date' is in format [XX]YYMMDD. 'time' is in format 'HHMMSS'
1552      * NOTE: This routine could do with some format checks.
1553      *
1554      * @param argument the date string
1555      */
1556     private Date getDateFrom(String argument) {
1557         if (argument == null) {
1558             throw new NNTPException("Date argument was absent.");
1559         }
1560         StringTokenizer tok = new StringTokenizer(argument, " ");
1561         if (tok.countTokens() < 2) {
1562             throw new NNTPException("Date argument was ill-formed.");
1563         }
1564         String date = tok.nextToken();
1565         String time = tok.nextToken();
1566         boolean utc = ( tok.hasMoreTokens() );
1567         Date d = new Date();
1568         try {
1569             StringBuffer dateStringBuffer =
1570                 new StringBuffer(64)
1571                     .append(date)
1572                     .append(" ")
1573                     .append(time);
1574             Date dt = DF_RFC977.parse(dateStringBuffer.toString());
1575             if ( utc ) {
1576                 dt = new Date(dt.getTime()+UTC_OFFSET);
1577             }
1578             return dt;
1579         } catch ( ParseException pe ) {
1580             StringBuffer exceptionBuffer =
1581                 new StringBuffer(128)
1582                     .append("Date extraction failed: ")
1583                     .append(date)
1584                     .append(",")
1585                     .append(time)
1586                     .append(",")
1587                     .append(utc);
1588             throw new NNTPException(exceptionBuffer.toString());
1589         }
1590     }
1591 
1592     /***
1593      * Returns the list of articles that match the range.
1594      *
1595      * A precondition of this method is that the selected
1596      * group be non-null.  The current article pointer must
1597      * be valid if no range is explicitly provided.
1598      *
1599      * @return null indicates insufficient information to
1600      * fetch the list of articles
1601      */
1602     private NNTPArticle[] getRange(String range) {
1603         // check for msg id
1604         if ( isMessageId(range)) {
1605             NNTPArticle article = theConfigData.getNNTPRepository().getArticleFromID(range);
1606             return ( article == null )
1607                 ? new NNTPArticle[0] : new NNTPArticle[] { article };
1608         }
1609 
1610         if ( range == null ) {
1611             range = "" + currentArticleNumber;
1612         }
1613 
1614         int start = -1;
1615         int end = -1;
1616         int idx = range.indexOf('-');
1617         if ( idx == -1 ) {
1618             start = Integer.parseInt(range);
1619             end = start;
1620         } else {
1621             start = Integer.parseInt(range.substring(0,idx));
1622             if ( (idx + 1) == range.length() ) {
1623                 end = group.getLastArticleNumber();
1624             } else {
1625                 end = Integer.parseInt(range.substring(idx + 1));
1626             }
1627         }
1628         List list = new ArrayList();
1629         for ( int i = start ; i <= end ; i++ ) {
1630             NNTPArticle article = group.getArticle(i);
1631             if ( article != null ) {
1632                 list.add(article);
1633             }
1634         }
1635         return (NNTPArticle[])list.toArray(new NNTPArticle[0]);
1636     }
1637 
1638     /***
1639      * Return whether the user associated with the connection (possibly no
1640      * user) is authorized to execute the command.
1641      *
1642      * @param the command being tested
1643      * @return whether the command is authorized
1644      */
1645     private boolean isAuthorized(String command) {
1646         isAlreadyAuthenticated = isAlreadyAuthenticated || isAuthenticated();
1647         if (isAlreadyAuthenticated) {
1648             return true;
1649         }
1650         // some commands are authorized, even if the user is not authenticated
1651         boolean allowed = command.equals("AUTHINFO");
1652         allowed = allowed || command.equals("MODE");
1653         allowed = allowed || command.equals("QUIT");
1654         return allowed;
1655     }
1656 
1657     /***
1658      * Return whether the connection has been authenticated.
1659      *
1660      * @return whether the connection has been authenticated.
1661      */
1662     private boolean isAuthenticated() {
1663         if ( theConfigData.isAuthRequired() ) {
1664             if  ((user != null) && (password != null) && (theConfigData.getUsersRepository() != null)) {
1665                 return theConfigData.getUsersRepository().test(user,password);
1666             } else {
1667                 return false;
1668             }
1669         } else {
1670             return true;
1671         }
1672     }
1673 
1674     /***
1675      * Tests a string to see whether it is formatted as a message
1676      * ID.
1677      *
1678      * @param testString the string to test
1679      *
1680      * @return whether the string is a candidate message ID
1681      */
1682     private static boolean isMessageId(String testString) {
1683         if ((testString != null) &&
1684             (testString.startsWith("<")) &&
1685             (testString.endsWith(">"))) {
1686             return true;
1687         } else {
1688             return false;
1689         }
1690    }
1691 
1692     /***
1693      * This method logs at a "DEBUG" level the response string that 
1694      * was sent to the SMTP client.  The method is provided largely
1695      * as syntactic sugar to neaten up the code base.  It is declared
1696      * private and final to encourage compiler inlining.
1697      *
1698      * @param responseString the response string sent to the client
1699      */
1700     private final void logResponseString(String responseString) {
1701         if (getLogger().isDebugEnabled()) {
1702             getLogger().debug("Sent: " + responseString);
1703         }
1704     }
1705 
1706     /***
1707      * Write and flush a response string.  The response is also logged.
1708      * Should be used for the last line of a multi-line response or
1709      * for a single line response.
1710      *
1711      * @param responseString the response string sent to the client
1712      */
1713     final void writeLoggedFlushedResponse(String responseString) {
1714         writer.println(responseString);
1715         writer.flush();
1716         logResponseString(responseString);
1717     }
1718 
1719     /***
1720      * Write a response string.  The response is also logged. 
1721      * Used for multi-line responses.
1722      *
1723      * @param responseString the response string sent to the client
1724      */
1725     final void writeLoggedResponse(String responseString) {
1726         writer.println(responseString);
1727         logResponseString(responseString);
1728     }
1729 
1730     /***
1731      * A private inner class which serves as an adaptor
1732      * between the WatchdogTarget interface and this
1733      * handler class.
1734      */
1735     private class NNTPWatchdogTarget
1736         implements WatchdogTarget {
1737 
1738         /***
1739          * @see org.apache.james.util.watchdog.WatchdogTarget#execute()
1740          */
1741         public void execute() {
1742             NNTPHandler.this.idleClose();
1743         }
1744 
1745     }
1746 }