View Javadoc

1   /**
2    *
3    * Copyright 2006 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.transport.nntp;
19  
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.OutputStream;
25  import java.io.PrintStream;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.net.InetAddress;
29  import java.net.Socket;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.StringTokenizer;
33  
34  import javax.mail.Message;
35  import javax.mail.MessagingException;
36  import javax.mail.Session;
37  
38  import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
39  import org.apache.geronimo.javamail.authentication.CramMD5Authenticator;
40  import org.apache.geronimo.javamail.authentication.DigestMD5Authenticator;
41  import org.apache.geronimo.javamail.authentication.LoginAuthenticator;
42  import org.apache.geronimo.javamail.authentication.PlainAuthenticator;
43  import org.apache.geronimo.javamail.util.MIMEOutputStream;
44  import org.apache.geronimo.javamail.util.TraceInputStream;
45  import org.apache.geronimo.javamail.util.TraceOutputStream;
46  import org.apache.geronimo.mail.util.Base64;
47  import org.apache.geronimo.mail.util.SessionUtil;
48  
49  /**
50   * Simple implementation of NNTP transport. Just does plain RFC977-ish delivery.
51   * <p/> There is no way to indicate failure for a given recipient (it's possible
52   * to have a recipient address rejected). The sun impl throws exceptions even if
53   * others successful), but maybe we do a different way... <p/>
54   * 
55   * @version $Rev: 432884 $ $Date: 2006-08-19 14:53:20 -0700 (Sat, 19 Aug 2006) $
56   */
57  public class NNTPConnection {
58  
59      /**
60       * constants for EOL termination
61       */
62      protected static final char CR = '\r';
63  
64      protected static final char LF = '\n';
65  
66      /**
67       * property keys for protocol properties.
68       */
69      protected static final String MAIL_NNTP_AUTH = "auth";
70  
71      protected static final String MAIL_NNTP_PORT = "port";
72  
73      protected static final String MAIL_NNTP_TIMEOUT = "timeout";
74  
75      protected static final String MAIL_NNTP_SASL_REALM = "sasl.realm";
76  
77      protected static final String MAIL_NNTP_FACTORY_CLASS = "socketFactory.class";
78  
79      protected static final String MAIL_NNTP_FACTORY_FALLBACK = "fallback";
80  
81      protected static final String MAIL_NNTP_LOCALADDRESS = "localaddress";
82  
83      protected static final String MAIL_NNTP_LOCALPORT = "localport";
84  
85      protected static final String MAIL_NNTP_QUITWAIT = "quitwait";
86  
87      protected static final String MAIL_NNTP_FACTORY_PORT = "socketFactory.port";
88  
89      protected static final String MAIL_NNTP_ENCODE_TRACE = "encodetrace";
90  
91      protected static final int MIN_MILLIS = 1000 * 60;
92  
93      protected static final int TIMEOUT = MIN_MILLIS * 5;
94  
95      protected static final String DEFAULT_MAIL_HOST = "localhost";
96  
97      protected static final int DEFAULT_NNTP_PORT = 119;
98  
99      protected static final String AUTHENTICATION_PLAIN = "PLAIN";
100 
101     protected static final String AUTHENTICATION_LOGIN = "LOGIN";
102 
103     protected static final String AUTHENTICATION_CRAMMD5 = "CRAM-MD5";
104 
105     protected static final String AUTHENTICATION_DIGESTMD5 = "DIGEST-MD5";
106 
107     // the protocol in use (either nntp or nntp-post).
108     String protocol;
109 
110     // the target host
111     protected String host;
112 
113     // the target server port.
114     protected int port;
115 
116     // the connection socket...can be a plain socket or SSLSocket, if TLS is
117     // being used.
118     protected Socket socket;
119 
120     // input stream used to read data. If Sasl is in use, this might be other
121     // than the
122     // direct access to the socket input stream.
123     protected InputStream inputStream;
124 
125     // the test reader wrapped around the input stream.
126     protected BufferedReader in;
127 
128     // the other end of the connection pipeline.
129     protected OutputStream outputStream;
130 
131     // does the server support posting?
132     protected boolean postingAllowed = true;
133 
134     // the username we connect with
135     protected String username;
136 
137     // the authentication password.
138     protected String password;
139 
140     // the target SASL realm (normally null unless explicitly set or we have an
141     // authentication mechanism that
142     // requires it.
143     protected String realm;
144 
145     // the last response line received from the server.
146     protected NNTPReply lastServerResponse = null;
147 
148     // our attached session
149     protected Session session;
150 
151     // our session provided debug output stream.
152     protected PrintStream debugStream;
153 
154     // our debug flag (passed from the hosting transport)
155     protected boolean debug;
156 
157     // list of authentication mechanisms supported by the server
158     protected HashMap serverAuthenticationMechanisms;
159 
160     // map of server extension arguments
161     protected HashMap serverExtensionArgs;
162 
163     // the welcome string from the server.
164     protected String welcomeString = null;
165 
166     /**
167      * Normal constructor for an NNTPConnection() object.
168      * 
169      * @param session
170      *            The attached session.
171      * @param host
172      *            The target host name of the NNTP server.
173      * @param port
174      *            The target listening port of the server. Defaults to 119 if
175      *            the port is specified as -1.
176      * @param username
177      *            The login user name (can be null unless authentication is
178      *            required).
179      * @param password
180      *            Password associated with the userid account. Can be null if
181      *            authentication is not required.
182      * @param debug
183      *            The session debug flag.
184      */
185     public NNTPConnection(String protocol, Session session, String host, int port, String username, String password,
186             boolean debug) {
187         this.protocol = protocol;
188         this.session = session;
189         this.host = host;
190         this.port = port;
191         this.username = username;
192         this.password = password;
193         this.debug = debug;
194 
195         // get our debug output.
196         debugStream = session.getDebugOut();
197     }
198 
199     /**
200      * Connect to the server and do the initial handshaking.
201      * 
202      * @exception MessagingException
203      */
204     public void connect() throws MessagingException {
205         try {
206 
207             // create socket and connect to server.
208             getConnection();
209 
210             // receive welcoming message
211             getWelcome();
212 
213         } catch (IOException e) {
214             if (debug) {
215                 debugOut("I/O exception establishing connection", e);
216             }
217             throw new MessagingException("Connection error", e);
218         }
219     }
220 
221     /**
222      * Close the connection. On completion, we'll be disconnected from the
223      * server and unable to send more data.
224      * 
225      * @exception MessagingException
226      */
227     public void close() throws MessagingException {
228         // if we're already closed, get outta here.
229         if (socket == null) {
230             return;
231         }
232         try {
233             // say goodbye
234             sendQuit();
235         } finally {
236             // and close up the connection. We do this in a finally block to
237             // make sure the connection
238             // is shut down even if quit gets an error.
239             closeServerConnection();
240         }
241     }
242 
243     /**
244      * Create a transport connection object and connect it to the target server.
245      * 
246      * @exception MessagingException
247      */
248     protected void getConnection() throws IOException {
249         // We might have been passed a socket to connect with...if not, we need
250         // to create one of the correct type.
251         if (socket == null) {
252             getConnectedSocket();
253         }
254         // if we already have a socket, get some information from it and
255         // override what we've been passed.
256         else {
257             port = socket.getPort();
258             host = socket.getInetAddress().getHostName();
259         }
260 
261         // now set up the input/output streams.
262         inputStream = new TraceInputStream(socket.getInputStream(), debugStream, debug, getBooleanProperty(
263                 MAIL_NNTP_ENCODE_TRACE, false));
264         ;
265         outputStream = new TraceOutputStream(socket.getOutputStream(), debugStream, debug, getBooleanProperty(
266                 MAIL_NNTP_ENCODE_TRACE, false));
267 
268         // get a reader to read the input as lines
269         in = new BufferedReader(new InputStreamReader(inputStream));
270     }
271 
272     /**
273      * Close the server connection at termination.
274      */
275     public void closeServerConnection() {
276         try {
277             socket.close();
278         } catch (IOException ignored) {
279         }
280 
281         socket = null;
282         inputStream = null;
283         outputStream = null;
284         in = null;
285     }
286 
287     /**
288      * Creates a connected socket
289      * 
290      * @exception MessagingException
291      */
292     public void getConnectedSocket() throws IOException {
293         if (debug) {
294             debugOut("Attempting plain socket connection to server " + host + ":" + port);
295         }
296 
297         // the socket factory can be specified via a session property. By
298         // default, we just directly
299         // instantiate a socket without using a factor.
300         String socketFactory = getProperty(MAIL_NNTP_FACTORY_CLASS);
301 
302         // there are several protocol properties that can be set to tune the
303         // created socket. We need to
304         // retrieve those bits before creating the socket.
305         int timeout = getIntProperty(MAIL_NNTP_TIMEOUT, -1);
306         InetAddress localAddress = null;
307         // see if we have a local address override.
308         String localAddrProp = getProperty(MAIL_NNTP_LOCALADDRESS);
309         if (localAddrProp != null) {
310             localAddress = InetAddress.getByName(localAddrProp);
311         }
312 
313         // check for a local port...default is to allow socket to choose.
314         int localPort = getIntProperty(MAIL_NNTP_LOCALPORT, 0);
315 
316         socket = null;
317 
318         // if there is no socket factory defined (normal), we just create a
319         // socket directly.
320         if (socketFactory == null) {
321             socket = new Socket(host, port, localAddress, localPort);
322         }
323 
324         else {
325             try {
326                 int socketFactoryPort = getIntProperty(MAIL_NNTP_FACTORY_PORT, -1);
327 
328                 // we choose the port used by the socket based on overrides.
329                 Integer portArg = new Integer(socketFactoryPort == -1 ? port : socketFactoryPort);
330 
331                 // use the current context loader to resolve this.
332                 ClassLoader loader = Thread.currentThread().getContextClassLoader();
333                 Class factoryClass = loader.loadClass(socketFactory);
334 
335                 // done indirectly, we need to invoke the method using
336                 // reflection.
337                 // This retrieves a factory instance.
338                 Method getDefault = factoryClass.getMethod("getDefault", new Class[0]);
339                 Object defFactory = getDefault.invoke(new Object(), new Object[0]);
340 
341                 // now that we have the factory, there are two different
342                 // createSocket() calls we use,
343                 // depending on whether we have a localAddress override.
344 
345                 if (localAddress != null) {
346                     // retrieve the createSocket(String, int, InetAddress, int)
347                     // method.
348                     Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE };
349                     Method createSocket = factoryClass.getMethod("createSocket", createSocketSig);
350 
351                     Object[] createSocketArgs = new Object[] { host, portArg, localAddress, new Integer(localPort) };
352                     socket = (Socket) createSocket.invoke(defFactory, createSocketArgs);
353                 } else {
354                     // retrieve the createSocket(String, int) method.
355                     Class[] createSocketSig = new Class[] { String.class, Integer.TYPE };
356                     Method createSocket = factoryClass.getMethod("createSocket", createSocketSig);
357 
358                     Object[] createSocketArgs = new Object[] { host, portArg };
359                     socket = (Socket) createSocket.invoke(defFactory, createSocketArgs);
360                 }
361             } catch (Throwable e) {
362                 // if a socket factor is specified, then we may need to fall
363                 // back to a default. This behavior
364                 // is controlled by (surprise) more session properties.
365                 if (getBooleanProperty(MAIL_NNTP_FACTORY_FALLBACK, false)) {
366                     if (debug) {
367                         debugOut("First plain socket attempt faile, falling back to default factory", e);
368                     }
369                     socket = new Socket(host, port, localAddress, localPort);
370                 }
371                 // we have an exception. We're going to throw an IOException,
372                 // which may require unwrapping
373                 // or rewrapping the exception.
374                 else {
375                     // we have an exception from the reflection, so unwrap the
376                     // base exception
377                     if (e instanceof InvocationTargetException) {
378                         e = ((InvocationTargetException) e).getTargetException();
379                     }
380 
381                     if (debug) {
382                         debugOut("Plain socket creation failure", e);
383                     }
384 
385                     // throw this as an IOException, with the original exception
386                     // attached.
387                     IOException ioe = new IOException("Error connecting to " + host + ", " + port);
388                     ioe.initCause(e);
389                     throw ioe;
390                 }
391             }
392         }
393 
394         if (timeout >= 0) {
395             socket.setSoTimeout(timeout);
396         }
397     }
398 
399     /**
400      * Get the servers welcome blob from the wire....
401      */
402     public void getWelcome() throws MessagingException {
403         NNTPReply line = getReply();
404 
405         //
406         if (line.isError()) {
407             throw new MessagingException("Error connecting to news server: " + line.getMessage());
408         }
409 
410         // remember we can post.
411         if (line.getCode() == NNTPReply.POSTING_ALLOWED) {
412             postingAllowed = true;
413         } else {
414             postingAllowed = false;
415         }
416 
417         // the NNTP store will want to use the welcome string, so save it.
418         welcomeString = line.getMessage();
419 
420         // find out what extensions this server supports.
421         getExtensions();
422     }
423 
424     /**
425      * Sends the QUIT message and receieves the response
426      */
427     public void sendQuit() throws MessagingException {
428         // there's yet another property that controls whether we should wait for
429         // a
430         // reply for a QUIT command. If on, just send the command and get outta
431         // here.
432         if (getBooleanProperty(MAIL_NNTP_QUITWAIT, false)) {
433             sendLine("QUIT");
434         } else {
435             // handle as a real command...we're going to ignore the response.
436             sendCommand("QUIT");
437         }
438     }
439 
440     /**
441      * Tell the server to switch to a named group.
442      * 
443      * @param name
444      *            The name of the target group.
445      * 
446      * @return The server response to the GROUP command.
447      */
448     public NNTPReply selectGroup(String name) throws MessagingException {
449         // send the GROUP command
450         return sendCommand("GROUP " + name);
451     }
452 
453     /**
454      * Ask the server what extensions it supports.
455      * 
456      * @return True if the command was accepted ok, false for any errors.
457      * @exception MessagingException
458      */
459     protected void getExtensions() throws MessagingException {
460         NNTPReply reply = sendCommand("LIST EXTENSIONS", NNTPReply.EXTENSIONS_SUPPORTED);
461 
462         // we get a 202 code back. The first line is just a greeting, and
463         // extensions are deliverd as data
464         // lines terminated with a "." line.
465         if (reply.getCode() != NNTPReply.EXTENSIONS_SUPPORTED) {
466             return;
467         }
468 
469         // get a fresh extension mapping table.
470         serverExtensionArgs = new HashMap();
471         serverAuthenticationMechanisms = new HashMap();
472 
473         // get the extension data lines.
474         List extensions = reply.getData();
475 
476         // process all of the continuation lines
477         for (int i = 0; i < extensions.size(); i++) {
478             // go process the extention
479             processExtension((String) extensions.get(i));
480         }
481     }
482 
483     /**
484      * Process an extension string passed back as the EHLP response.
485      * 
486      * @param extension
487      *            The string value of the extension (which will be of the form
488      *            "NAME arguments").
489      */
490     protected void processExtension(String extension) {
491         String extensionName = extension.toUpperCase();
492         String argument = "";
493 
494         int delimiter = extension.indexOf(' ');
495         // if we have a keyword with arguments, parse them out and add to the
496         // argument map.
497         if (delimiter != -1) {
498             extensionName = extension.substring(0, delimiter).toUpperCase();
499             argument = extension.substring(delimiter + 1);
500         }
501 
502         // add this to the map so it can be tested later.
503         serverExtensionArgs.put(extensionName, argument);
504 
505         // process a few special ones that don't require extra parsing.
506         // AUTHINFO is entered in as a auth mechanism.
507         if (extensionName.equals("AUTHINFO")) {
508             serverAuthenticationMechanisms.put("AUTHINFO", "AUTHINFO");
509         }
510         // special case for some older servers.
511         else if (extensionName.equals("SASL")) {
512             // The security mechanisms are blank delimited tokens.
513             StringTokenizer tokenizer = new StringTokenizer(argument);
514 
515             while (tokenizer.hasMoreTokens()) {
516                 String mechanism = tokenizer.nextToken().toUpperCase();
517                 serverAuthenticationMechanisms.put(mechanism, mechanism);
518             }
519         }
520     }
521 
522     /**
523      * Retrieve any argument information associated with a extension reported
524      * back by the server on the EHLO command.
525      * 
526      * @param name
527      *            The name of the target server extension.
528      * 
529      * @return Any argument passed on a server extension. Returns null if the
530      *         extension did not include an argument or the extension was not
531      *         supported.
532      */
533     public String extensionParameter(String name) {
534         if (serverExtensionArgs != null) {
535             return (String) serverExtensionArgs.get(name);
536         }
537         return null;
538     }
539 
540     /**
541      * Tests whether the target server supports a named extension.
542      * 
543      * @param name
544      *            The target extension name.
545      * 
546      * @return true if the target server reported on the EHLO command that is
547      *         supports the targer server, false if the extension was not
548      *         supported.
549      */
550     public boolean supportsExtension(String name) {
551         // this only returns null if we don't have this extension
552         return extensionParameter(name) != null;
553     }
554 
555     /**
556      * Determine if the target server supports a given authentication mechanism.
557      * 
558      * @param mechanism
559      *            The mechanism name.
560      * 
561      * @return true if the server EHLO response indicates it supports the
562      *         mechanism, false otherwise.
563      */
564     protected boolean supportsAuthentication(String mechanism) {
565         return serverAuthenticationMechanisms.get(mechanism) != null;
566     }
567 
568     /**
569      * Sends the data in the message down the socket. This presumes the server
570      * is in the right place and ready for getting the DATA message and the data
571      * right place in the sequence
572      */
573     public synchronized void sendPost(Message msg) throws MessagingException {
574 
575         // send the POST command
576         NNTPReply line = sendCommand("POST");
577 
578         if (line.getCode() != NNTPReply.SEND_ARTICLE) {
579             throw new MessagingException("Server rejected POST command: " + line);
580         }
581 
582         // we've received permission to send the data, so ask the message to
583         // write itself out.
584         try {
585             // the data content has two requirements we need to meet by
586             // filtering the
587             // output stream. Requirement 1 is to conicalize any line breaks.
588             // All line
589             // breaks will be transformed into properly formed CRLF sequences.
590             //
591             // Requirement 2 is to perform byte-stuff for any line that begins
592             // with a "."
593             // so that data is not confused with the end-of-data marker (a
594             // "\r\n.\r\n" sequence.
595             //
596             // The MIME output stream performs those two functions on behalf of
597             // the content
598             // writer.
599             OutputStream mimeOut = new MIMEOutputStream(outputStream);
600 
601             msg.writeTo(mimeOut);
602             mimeOut.flush();
603         } catch (IOException e) {
604             throw new MessagingException("I/O error posting message", e);
605         } catch (MessagingException e) {
606             throw new MessagingException("Exception posting message", e);
607         }
608 
609         // now to finish, we send a CRLF sequence, followed by a ".".
610         sendLine("");
611         sendLine(".");
612 
613         // use a longer time out here to give the server time to process the
614         // data.
615         line = new NNTPReply(receiveLine());
616 
617         if (line.getCode() != NNTPReply.POSTED_OK) {
618             throw new MessagingException("Server rejected POST command: " + line);
619         }
620     }
621 
622     /**
623      * Issue a command and retrieve the response. If the given success indicator
624      * is received, the command is returning a longer response, terminated by a
625      * "crlf.crlf" sequence. These lines are attached to the reply.
626      * 
627      * @param command
628      *            The command to issue.
629      * @param success
630      *            The command reply that indicates additional data should be
631      *            retrieved.
632      * 
633      * @return The command reply.
634      */
635     public synchronized NNTPReply sendCommand(String command, int success) throws MessagingException {
636         NNTPReply reply = sendCommand(command);
637         if (reply.getCode() == success) {
638             reply.retrieveData(in);
639         }
640         return reply;
641     }
642 
643     /**
644      * Send a command to the server, returning the first response line back as a
645      * reply.
646      * 
647      * @param data
648      *            The data to send.
649      * 
650      * @return A reply object with the reply line.
651      * @exception MessagingException
652      */
653     public NNTPReply sendCommand(String data) throws MessagingException {
654         sendLine(data);
655         NNTPReply reply = getReply();
656         // did the server just inform us we need to authenticate? The spec
657         // allows this
658         // response to be sent at any time, so we need to try to authenticate
659         // and then retry the command.
660         if (reply.getCode() == NNTPReply.AUTHINFO_REQUIRED || reply.getCode() == NNTPReply.AUTHINFO_SIMPLE_REQUIRED) {
661             if (debug) {
662                 debugOut("Authentication required received from server.");
663             }
664             // authenticate with the server, if necessary
665             processAuthentication(reply.getCode());
666             // if we've safely authenticated, we can reissue the command and
667             // process the response.
668             sendLine(data);
669             reply = getReply();
670         }
671         return reply;
672     }
673 
674     /**
675      * Send a command to the server, returning the first response line back as a
676      * reply.
677      * 
678      * @param data
679      *            The data to send.
680      * 
681      * @return A reply object with the reply line.
682      * @exception MessagingException
683      */
684     public NNTPReply sendAuthCommand(String data) throws MessagingException {
685         sendLine(data);
686         return getReply();
687     }
688 
689     /**
690      * Sends a message down the socket and terminates with the appropriate CRLF
691      */
692     public void sendLine(String data) throws MessagingException {
693         if (socket == null || !socket.isConnected()) {
694             throw new MessagingException("no connection");
695         }
696         try {
697             outputStream.write(data.getBytes());
698             outputStream.write(CR);
699             outputStream.write(LF);
700             outputStream.flush();
701         } catch (IOException e) {
702             throw new MessagingException(e.toString());
703         }
704     }
705 
706     /**
707      * Get a reply line for an NNTP command.
708      * 
709      * @return An NNTP reply object from the stream.
710      */
711     public NNTPReply getReply() throws MessagingException {
712         lastServerResponse = new NNTPReply(receiveLine());
713         return lastServerResponse;
714     }
715 
716     /**
717      * Retrieve the last response received from the NNTP server.
718      * 
719      * @return The raw response string (including the error code) returned from
720      *         the NNTP server.
721      */
722     public String getLastServerResponse() {
723         if (lastServerResponse == null) {
724             return "";
725         }
726         return lastServerResponse.getReply();
727     }
728 
729     /**
730      * Receives one line from the server. A line is a sequence of bytes
731      * terminated by a CRLF
732      * 
733      * @return the line from the server as String
734      */
735     public String receiveLine() throws MessagingException {
736         if (socket == null || !socket.isConnected()) {
737             throw new MessagingException("no connection");
738         }
739 
740         try {
741             String line = in.readLine();
742             if (line == null) {
743                 throw new MessagingException("Unexpected end of stream");
744             }
745             return line;
746         } catch (IOException e) {
747             throw new MessagingException("Error reading from server", e);
748         }
749     }
750 
751     /**
752      * Retrieve the SASL realm used for DIGEST-MD5 authentication. This will
753      * either be explicitly set, or retrieved using the mail.nntp.sasl.realm
754      * session property.
755      * 
756      * @return The current realm information (which can be null).
757      */
758     public String getSASLRealm() {
759         // if the realm is null, retrieve it using the realm session property.
760         if (realm == null) {
761             realm = getProperty(MAIL_NNTP_SASL_REALM);
762         }
763         return realm;
764     }
765 
766     /**
767      * Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton.
768      * 
769      * @param name
770      *            The new realm name.
771      */
772     public void setSASLRealm(String name) {
773         realm = name;
774     }
775 
776     /**
777      * Authenticate with the server, if necessary (or possible).
778      */
779     protected void processAuthentication(int request) throws MessagingException {
780         // we need to authenticate, but we don't have userid/password
781         // information...fail this
782         // immediately.
783         if (username == null || password == null) {
784             throw new MessagingException("Server requires user authentication");
785         }
786 
787         if (request == NNTPReply.AUTHINFO_SIMPLE_REQUIRED) {
788             processAuthinfoSimple();
789         } else {
790             if (!processAuthinfoSasl()) {
791                 processAuthinfoUser();
792             }
793         }
794     }
795 
796     /**
797      * Process an AUTHINFO SIMPLE command. Not widely used, but if the server
798      * asks for it, we can respond.
799      * 
800      * @exception MessagingException
801      */
802     protected void processAuthinfoSimple() throws MessagingException {
803         NNTPReply reply = sendAuthCommand("AUTHINFO SIMPLE");
804         if (reply.getCode() != NNTPReply.AUTHINFO_CONTINUE) {
805             throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
806         }
807         reply = sendAuthCommand(username + " " + password);
808         if (reply.getCode() != NNTPReply.AUTHINFO_ACCEPTED) {
809             throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
810         }
811     }
812 
813     /**
814      * Process AUTHINFO GENERIC. Right now, this appears not to be widely used
815      * and information on how the conversations are handled for different auth
816      * types is lacking, so right now, this just returns false to force the
817      * userid/password form to be used.
818      * 
819      * @return Always returns false.
820      * @exception MessagingException
821      */
822     protected boolean processAuthinfoGeneric() throws MessagingException {
823         return false;
824     }
825 
826     /**
827      * Process AUTHINFO SASL.
828      * 
829      * @return Returns true if the server support a SASL authentication
830      *         mechanism and accepted reponse challenges.
831      * @exception MessagingException
832      */
833     protected boolean processAuthinfoSasl() throws MessagingException {
834         ClientAuthenticator authenticator = null;
835 
836         // now go through the progression of mechanisms we support, from the
837         // most secure to the
838         // least secure.
839 
840         if (supportsAuthentication(AUTHENTICATION_DIGESTMD5)) {
841             authenticator = new DigestMD5Authenticator(host, username, password, getSASLRealm());
842         } else if (supportsAuthentication(AUTHENTICATION_CRAMMD5)) {
843             authenticator = new CramMD5Authenticator(username, password);
844         } else if (supportsAuthentication(AUTHENTICATION_LOGIN)) {
845             authenticator = new LoginAuthenticator(username, password);
846         } else if (supportsAuthentication(AUTHENTICATION_PLAIN)) {
847             authenticator = new PlainAuthenticator(username, password);
848         } else {
849             // can't find a mechanism we support in common
850             return false;
851         }
852 
853         if (debug) {
854             debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
855         }
856 
857         // if the authenticator has some initial data, we compose a command
858         // containing the initial data.
859         if (authenticator.hasInitialResponse()) {
860             StringBuffer command = new StringBuffer();
861             // the auth command initiates the handshaking.
862             command.append("AUTHINFO SASL ");
863             // and tell the server which mechanism we're using.
864             command.append(authenticator.getMechanismName());
865             command.append(" ");
866             // and append the response data
867             command.append(new String(Base64.encode(authenticator.evaluateChallenge(null))));
868             // send the command now
869             sendLine(command.toString());
870         }
871         // we just send an auth command with the command type.
872         else {
873             StringBuffer command = new StringBuffer();
874             // the auth command initiates the handshaking.
875             command.append("AUTHINFO SASL");
876             // and tell the server which mechanism we're using.
877             command.append(authenticator.getMechanismName());
878             // send the command now
879             sendLine(command.toString());
880         }
881 
882         // now process the challenge sequence. We get a 235 response back when
883         // the server accepts the
884         // authentication, and a 334 indicates we have an additional challenge.
885         while (true) {
886             // get the next line, and if it is an error response, return now.
887             NNTPReply line = getReply();
888 
889             // if we get a completion return, we've passed muster, so give an
890             // authentication response.
891             if (line.getCode() == NNTPReply.AUTHINFO_ACCEPTED || line.getCode() == NNTPReply.AUTHINFO_ACCEPTED_FINAL) {
892                 if (debug) {
893                     debugOut("Successful SMTP authentication");
894                 }
895                 return true;
896             }
897             // we have an additional challenge to process.
898             else if (line.getCode() == NNTPReply.AUTHINFO_CHALLENGE) {
899                 // Does the authenticator think it is finished? We can't answer
900                 // an additional challenge,
901                 // so fail this.
902                 if (authenticator.isComplete()) {
903                     if (debug) {
904                         debugOut("Extra authentication challenge " + line);
905                     }
906                     return false;
907                 }
908 
909                 // we're passed back a challenge value, Base64 encoded.
910                 byte[] challenge = Base64.decode(line.getMessage().getBytes());
911 
912                 // have the authenticator evaluate and send back the encoded
913                 // response.
914                 sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge))));
915             }
916             // completion or challenge are the only responses we know how to
917             // handle. Anything else must
918             // be a failure.
919             else {
920                 if (debug) {
921                     debugOut("Authentication failure " + line);
922                 }
923                 return false;
924             }
925         }
926     }
927 
928     /**
929      * Process an AUTHINFO USER command. Most common form of NNTP
930      * authentication.
931      * 
932      * @exception MessagingException
933      */
934     protected void processAuthinfoUser() throws MessagingException {
935         NNTPReply reply = sendAuthCommand("AUTHINFO USER " + username);
936         // accepted without a password (uncommon, but allowed), we're done
937         if (reply.getCode() == NNTPReply.AUTHINFO_ACCEPTED) {
938             return;
939         }
940         // the only other non-error response is continue.
941         if (reply.getCode() != NNTPReply.AUTHINFO_CONTINUE) {
942             throw new MessagingException("Error authenticating with server using AUTHINFO USER: " + reply);
943         }
944         // now send the password. We expect an accepted response.
945         reply = sendAuthCommand("AUTHINFO PASS " + password);
946         if (reply.getCode() != NNTPReply.AUTHINFO_ACCEPTED) {
947             throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
948         }
949     }
950 
951     /**
952      * Internal debug output routine.
953      * 
954      * @param value
955      *            The string value to output.
956      */
957     protected void debugOut(String message) {
958         debugStream.println("NNTPTransport DEBUG: " + message);
959     }
960 
961     /**
962      * Internal debugging routine for reporting exceptions.
963      * 
964      * @param message
965      *            A message associated with the exception context.
966      * @param e
967      *            The received exception.
968      */
969     protected void debugOut(String message, Throwable e) {
970         debugOut("Received exception -> " + message);
971         debugOut("Exception message -> " + e.getMessage());
972         e.printStackTrace(debugStream);
973     }
974 
975     /**
976      * Indicate whether posting is allowed for a given server.
977      * 
978      * @return True if the server allows posting, false if the server is
979      *         read-only.
980      */
981     public boolean isPostingAllowed() {
982         return postingAllowed;
983     }
984 
985     /**
986      * Retrieve the welcome string sent back from the server.
987      * 
988      * @return The server provided welcome string.
989      */
990     public String getWelcomeString() {
991         return welcomeString;
992     }
993 
994     /**
995      * Return the server host for this connection.
996      * 
997      * @return The String name of the server host.
998      */
999     public String getHost() {
1000         return host;
1001     }
1002 
1003     /**
1004      * Get a property associated with this mail protocol.
1005      * 
1006      * @param name
1007      *            The name of the property.
1008      * 
1009      * @return The property value (returns null if the property has not been
1010      *         set).
1011      */
1012     String getProperty(String name) {
1013         // the name we're given is the least qualified part of the name. We
1014         // construct the full property name
1015         // using the protocol (either "nntp" or "nntp-post").
1016         String fullName = "mail." + protocol + "." + name;
1017         return session.getProperty(fullName);
1018     }
1019 
1020     /**
1021      * Get a property associated with this mail session. Returns the provided
1022      * default if it doesn't exist.
1023      * 
1024      * @param name
1025      *            The name of the property.
1026      * @param defaultValue
1027      *            The default value to return if the property doesn't exist.
1028      * 
1029      * @return The property value (returns defaultValue if the property has not
1030      *         been set).
1031      */
1032     String getProperty(String name, String defaultValue) {
1033         // the name we're given is the least qualified part of the name. We
1034         // construct the full property name
1035         // using the protocol (either "nntp" or "nntp-post").
1036         String fullName = "mail." + protocol + "." + name;
1037         return SessionUtil.getProperty(session, fullName, defaultValue);
1038     }
1039 
1040     /**
1041      * Get a property associated with this mail session as an integer value.
1042      * Returns the default value if the property doesn't exist or it doesn't
1043      * have a valid int value.
1044      * 
1045      * @param name
1046      *            The name of the property.
1047      * @param defaultValue
1048      *            The default value to return if the property doesn't exist.
1049      * 
1050      * @return The property value converted to an int.
1051      */
1052     int getIntProperty(String name, int defaultValue) {
1053         // the name we're given is the least qualified part of the name. We
1054         // construct the full property name
1055         // using the protocol (either "nntp" or "nntp-post").
1056         String fullName = "mail." + protocol + "." + name;
1057         return SessionUtil.getIntProperty(session, fullName, defaultValue);
1058     }
1059 
1060     /**
1061      * Get a property associated with this mail session as an boolean value.
1062      * Returns the default value if the property doesn't exist or it doesn't
1063      * have a valid int value.
1064      * 
1065      * @param name
1066      *            The name of the property.
1067      * @param defaultValue
1068      *            The default value to return if the property doesn't exist.
1069      * 
1070      * @return The property value converted to a boolean
1071      */
1072     boolean getBooleanProperty(String name, boolean defaultValue) {
1073         // the name we're given is the least qualified part of the name. We
1074         // construct the full property name
1075         // using the protocol (either "nntp" or "nntp-post").
1076         String fullName = "mail." + protocol + "." + name;
1077         return SessionUtil.getBooleanProperty(session, fullName, defaultValue);
1078     }
1079 }