View Javadoc

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