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