View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.geronimo.javamail.transport.nntp;
21  
22  import java.io.BufferedReader;
23  import java.io.BufferedOutputStream; 
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.io.OutputStream;
28  import java.io.PrintStream;
29  import java.io.PrintWriter;
30  import java.lang.reflect.InvocationTargetException;
31  import java.lang.reflect.Method;
32  import java.net.InetAddress;
33  import java.net.Socket;
34  import java.util.ArrayList; 
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.StringTokenizer;
38  
39  import javax.mail.Message;
40  import javax.mail.MessagingException;
41  import javax.mail.Session;
42  
43  import org.apache.geronimo.javamail.authentication.ClientAuthenticator; 
44  import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 
45  import org.apache.geronimo.javamail.util.MailConnection; 
46  import org.apache.geronimo.javamail.util.MIMEOutputStream;
47  import org.apache.geronimo.javamail.util.ProtocolProperties; 
48  import org.apache.geronimo.mail.util.Base64;
49  import org.apache.geronimo.mail.util.SessionUtil;
50  
51  /**
52   * Simple implementation of NNTP transport. Just does plain RFC977-ish delivery.
53   * 
54   * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $
55   */
56  public class NNTPConnection extends MailConnection {
57  
58      /**
59       * constants for EOL termination
60       */
61      protected static final char CR = '\r';
62  
63      protected static final char LF = '\n';
64  
65      /**
66       * property keys for protocol properties.
67       */
68      protected static final int DEFAULT_NNTP_PORT = 119;
69      // does the server support posting?
70      protected boolean postingAllowed = true;
71      
72      // different authentication mechanisms 
73      protected boolean authInfoUserAllowed = false; 
74      protected boolean authInfoSaslAllowed = false; 
75      
76      // the last response line received from the server.
77      protected NNTPReply lastServerResponse = null;
78  
79      // the welcome string from the server.
80      protected String welcomeString = null;
81      
82      // input reader wrapped around the socket input stream 
83      protected BufferedReader reader; 
84      // output writer wrapped around the socket output stream. 
85      protected PrintWriter writer; 
86  
87      /**
88       * Normal constructor for an NNTPConnection() object.
89       * 
90       * @param props  The property bundle for this protocol instance.
91       */
92      public NNTPConnection(ProtocolProperties props) {
93          super(props);
94      }
95      
96      
97      /**
98       * Connect to the server and do the initial handshaking.
99       * 
100      * @param host     The target host name.
101      * @param port     The target port
102      * @param username The connection username (can be null)
103      * @param password The authentication password (can be null).
104      * 
105      * @return true if we were able to obtain a connection and 
106      *         authenticate.
107      * @exception MessagingException
108      */
109     public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException {
110         super.protocolConnect(host, port, username, password); 
111         // create socket and connect to server.
112         getConnection();
113 
114         // receive welcoming message
115         getWelcome();
116         
117         return true; 
118     }
119 
120 
121     /**
122      * Create a transport connection object and connect it to the
123      * target server.
124      *
125      * @exception MessagingException
126      */
127     protected void getConnection() throws MessagingException
128     {
129         try {
130             // do all of the non-protocol specific set up.  This will get our socket established 
131             // and ready use. 
132             super.getConnection(); 
133         } catch (IOException e) {
134             throw new MessagingException("Unable to obtain a connection to the NNTP server", e); 
135         }
136         
137         // The NNTP protocol is inherently a string-based protocol, so we get 
138         // string readers/writers for the connection streams 
139         reader = new BufferedReader(new InputStreamReader(inputStream));
140         writer = new PrintWriter(new BufferedOutputStream(outputStream));
141     }
142     
143 
144     /**
145      * Close the connection. On completion, we'll be disconnected from the
146      * server and unable to send more data.
147      * 
148      * @exception MessagingException
149      */
150     public void close() throws MessagingException {
151         // if we're already closed, get outta here.
152         if (socket == null) {
153             return;
154         }
155         try {
156             // say goodbye
157             sendQuit();
158         } finally {
159             // and close up the connection. We do this in a finally block to
160             // make sure the connection
161             // is shut down even if quit gets an error.
162             closeServerConnection();
163             // get rid of our response processor too. 
164             reader = null; 
165             writer = null; 
166         }
167     }
168 
169     public String toString() {
170         return "NNTPConnection host: " + serverHost + " port: " + serverPort;
171     }
172     
173 
174     /**
175      * Get the servers welcome blob from the wire....
176      */
177     public void getWelcome() throws MessagingException {
178         NNTPReply line = getReply();
179 
180         //
181         if (line.isError()) {
182             throw new MessagingException("Error connecting to news server: " + line.getMessage());
183         }
184 
185         // remember we can post.
186         if (line.getCode() == NNTPReply.POSTING_ALLOWED) {
187             postingAllowed = true;
188         } else {
189             postingAllowed = false;
190         }
191 
192         // the NNTP store will want to use the welcome string, so save it.
193         welcomeString = line.getMessage();
194 
195         // find out what extensions this server supports.
196         getExtensions();
197     }
198 
199     
200     /**
201      * Sends the QUIT message and receieves the response
202      */
203     public void sendQuit() throws MessagingException {
204         sendLine("QUIT");
205     }
206     
207 
208     /**
209      * Tell the server to switch to a named group.
210      * 
211      * @param name
212      *            The name of the target group.
213      * 
214      * @return The server response to the GROUP command.
215      */
216     public NNTPReply selectGroup(String name) throws MessagingException {
217         // send the GROUP command
218         return sendCommand("GROUP " + name);
219     }
220     
221 
222     /**
223      * Ask the server what extensions it supports.
224      * 
225      * @return True if the command was accepted ok, false for any errors.
226      * @exception MessagingException
227      */
228     protected void getExtensions() throws MessagingException {
229         NNTPReply reply = sendCommand("LIST EXTENSIONS", NNTPReply.EXTENSIONS_SUPPORTED);
230 
231         // we get a 202 code back. The first line is just a greeting, and
232         // extensions are delivered as data
233         // lines terminated with a "." line.
234         if (reply.getCode() != NNTPReply.EXTENSIONS_SUPPORTED) {
235             return;
236         }
237 
238         // get a fresh extension mapping table.
239         capabilities = new HashMap();
240         authentications = new ArrayList(); 
241 
242         // get the extension data lines.
243         List extensions = reply.getData();
244 
245         // process all of the continuation lines
246         for (int i = 0; i < extensions.size(); i++) {
247             // go process the extention
248             processExtension((String) extensions.get(i));
249         }
250     }
251 
252     
253     /**
254      * Process an extension string passed back as the LIST EXTENSIONS response.
255      * 
256      * @param extension
257      *            The string value of the extension (which will be of the form
258      *            "NAME arguments").
259      */
260     protected void processExtension(String extension) {
261         String extensionName = extension.toUpperCase();
262         String argument = "";
263 
264         int delimiter = extension.indexOf(' ');
265         // if we have a keyword with arguments, parse them out and add to the
266         // argument map.
267         if (delimiter != -1) {
268             extensionName = extension.substring(0, delimiter).toUpperCase();
269             argument = extension.substring(delimiter + 1);
270         }
271 
272         // add this to the map so it can be tested later.
273         capabilities.put(extensionName, argument);
274 
275         // we need to determine which authentication mechanisms are supported here 
276         if (extensionName.equals("AUTHINFO")) {
277             StringTokenizer tokenizer = new StringTokenizer(argument); 
278             
279             while (tokenizer.hasMoreTokens()) {
280                 // we only know how to do USER or SASL 
281                 String mechanism = tokenizer.nextToken().toUpperCase();
282                 if (mechanism.equals("SASL")) {
283                     authInfoSaslAllowed = true; 
284                 }
285                 else if (mechanism.equals("USER")) {
286                     authInfoUserAllowed = true; 
287                 }
288             }
289         }
290         // special case for some older servers.
291         else if (extensionName.equals("SASL")) {
292             // The security mechanisms are blank delimited tokens.
293             StringTokenizer tokenizer = new StringTokenizer(argument);
294 
295             while (tokenizer.hasMoreTokens()) {
296                 String mechanism = tokenizer.nextToken().toUpperCase();
297                 authentications.add(mechanism);
298             }
299         }
300     }
301     
302 
303     /**
304      * Retrieve any argument information associated with a extension reported
305      * back by the server on the EHLO command.
306      * 
307      * @param name
308      *            The name of the target server extension.
309      * 
310      * @return Any argument passed on a server extension. Returns null if the
311      *         extension did not include an argument or the extension was not
312      *         supported.
313      */
314     public String extensionParameter(String name) {
315         if (capabilities != null) {
316             return (String) capabilities.get(name);
317         }
318         return null;
319     }
320 
321     /**
322      * Tests whether the target server supports a named extension.
323      * 
324      * @param name
325      *            The target extension name.
326      * 
327      * @return true if the target server reported on the EHLO command that is
328      *         supports the targer server, false if the extension was not
329      *         supported.
330      */
331     public boolean supportsExtension(String name) {
332         // this only returns null if we don't have this extension
333         return extensionParameter(name) != null;
334     }
335 
336     
337     /**
338      * Sends the data in the message down the socket. This presumes the server
339      * is in the right place and ready for getting the DATA message and the data
340      * right place in the sequence
341      */
342     public synchronized void sendPost(Message msg) throws MessagingException {
343 
344         // send the POST command
345         NNTPReply line = sendCommand("POST");
346 
347         if (line.getCode() != NNTPReply.SEND_ARTICLE) {
348             throw new MessagingException("Server rejected POST command: " + line);
349         }
350 
351         // we've received permission to send the data, so ask the message to
352         // write itself out.
353         try {
354             // the data content has two requirements we need to meet by
355             // filtering the
356             // output stream. Requirement 1 is to conicalize any line breaks.
357             // All line
358             // breaks will be transformed into properly formed CRLF sequences.
359             //
360             // Requirement 2 is to perform byte-stuff for any line that begins
361             // with a "."
362             // so that data is not confused with the end-of-data marker (a
363             // "\r\n.\r\n" sequence.
364             //
365             // The MIME output stream performs those two functions on behalf of
366             // the content
367             // writer.
368             MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream);
369 
370             msg.writeTo(mimeOut);
371 
372             // now to finish, we send a CRLF sequence, followed by a ".".
373             mimeOut.writeSMTPTerminator();           
374             // and flush the data to send it along 
375             mimeOut.flush();   
376         } catch (IOException e) {
377             throw new MessagingException("I/O error posting message", e);
378         } catch (MessagingException e) {
379             throw new MessagingException("Exception posting message", e);
380         }
381 
382         // use a longer time out here to give the server time to process the
383         // data.
384         line = new NNTPReply(receiveLine());
385 
386         if (line.getCode() != NNTPReply.POSTED_OK) {
387             throw new MessagingException("Server rejected POST command: " + line);
388         }
389     }
390 
391     /**
392      * Issue a command and retrieve the response. If the given success indicator
393      * is received, the command is returning a longer response, terminated by a
394      * "crlf.crlf" sequence. These lines are attached to the reply.
395      * 
396      * @param command
397      *            The command to issue.
398      * @param success
399      *            The command reply that indicates additional data should be
400      *            retrieved.
401      * 
402      * @return The command reply.
403      */
404     public synchronized NNTPReply sendCommand(String command, int success) throws MessagingException {
405         NNTPReply reply = sendCommand(command);
406         if (reply.getCode() == success) {
407             reply.retrieveData(reader);
408         }
409         return reply;
410     }
411 
412     /**
413      * Send a command to the server, returning the first response line back as a
414      * reply.
415      * 
416      * @param data
417      *            The data to send.
418      * 
419      * @return A reply object with the reply line.
420      * @exception MessagingException
421      */
422     public NNTPReply sendCommand(String data) throws MessagingException {
423         sendLine(data);
424         NNTPReply reply = getReply();
425         // did the server just inform us we need to authenticate? The spec
426         // allows this
427         // response to be sent at any time, so we need to try to authenticate
428         // and then retry the command.
429         if (reply.getCode() == NNTPReply.AUTHINFO_REQUIRED || reply.getCode() == NNTPReply.AUTHINFO_SIMPLE_REQUIRED) {
430             debugOut("Authentication required received from server.");
431             // authenticate with the server, if necessary
432             processAuthentication(reply.getCode());
433             // if we've safely authenticated, we can reissue the command and
434             // process the response.
435             sendLine(data);
436             reply = getReply();
437         }
438         return reply;
439     }
440 
441     /**
442      * Send a command to the server, returning the first response line back as a
443      * reply.
444      * 
445      * @param data
446      *            The data to send.
447      * 
448      * @return A reply object with the reply line.
449      * @exception MessagingException
450      */
451     public NNTPReply sendAuthCommand(String data) throws MessagingException {
452         sendLine(data);
453         return getReply();
454     }
455 
456     /**
457      * Sends a message down the socket and terminates with the appropriate CRLF
458      */
459     public void sendLine(String data) throws MessagingException {
460         if (socket == null || !socket.isConnected()) {
461             throw new MessagingException("no connection");
462         }
463         try {
464             outputStream.write(data.getBytes());
465             outputStream.write(CR);
466             outputStream.write(LF);
467             outputStream.flush();
468         } catch (IOException e) {
469             throw new MessagingException(e.toString());
470         }
471     }
472 
473     /**
474      * Get a reply line for an NNTP command.
475      * 
476      * @return An NNTP reply object from the stream.
477      */
478     public NNTPReply getReply() throws MessagingException {
479         lastServerResponse = new NNTPReply(receiveLine());
480         return lastServerResponse;
481     }
482 
483     /**
484      * Retrieve the last response received from the NNTP server.
485      * 
486      * @return The raw response string (including the error code) returned from
487      *         the NNTP server.
488      */
489     public String getLastServerResponse() {
490         if (lastServerResponse == null) {
491             return "";
492         }
493         return lastServerResponse.getReply();
494     }
495 
496     /**
497      * Receives one line from the server. A line is a sequence of bytes
498      * terminated by a CRLF
499      * 
500      * @return the line from the server as String
501      */
502     public String receiveLine() throws MessagingException {
503         if (socket == null || !socket.isConnected()) {
504             throw new MessagingException("no connection");
505         }
506 
507         try {
508             String line = reader.readLine();
509             if (line == null) {
510                 throw new MessagingException("Unexpected end of stream");
511             }
512             return line;
513         } catch (IOException e) {
514             throw new MessagingException("Error reading from server", e);
515         }
516     }
517 
518     
519     /**
520      * Authenticate with the server, if necessary (or possible).
521      */
522     protected void processAuthentication(int request) throws MessagingException {
523         // we need to authenticate, but we don't have userid/password
524         // information...fail this
525         // immediately.
526         if (username == null || password == null) {
527             throw new MessagingException("Server requires user authentication");
528         }
529 
530         if (request == NNTPReply.AUTHINFO_SIMPLE_REQUIRED) {
531             processAuthinfoSimple();
532         } else {
533             if (!processSaslAuthentication()) {
534                 processAuthinfoUser();
535             }
536         }
537     }
538 
539     /**
540      * Process an AUTHINFO SIMPLE command. Not widely used, but if the server
541      * asks for it, we can respond.
542      * 
543      * @exception MessagingException
544      */
545     protected void processAuthinfoSimple() throws MessagingException {
546         NNTPReply reply = sendAuthCommand("AUTHINFO SIMPLE");
547         if (reply.getCode() != NNTPReply.AUTHINFO_CONTINUE) {
548             throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
549         }
550         reply = sendAuthCommand(username + " " + password);
551         if (reply.getCode() != NNTPReply.AUTHINFO_ACCEPTED) {
552             throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
553         }
554     }
555 
556     
557     /**
558      * Process SASL-type authentication.
559      * 
560      * @return Returns true if the server support a SASL authentication mechanism and
561      *         accepted reponse challenges.
562      * @exception MessagingException
563      */
564     protected boolean processSaslAuthentication() throws MessagingException {
565         // only do this if permitted 
566         if (!authInfoSaslAllowed) {
567             return false; 
568         }
569         // if unable to get an appropriate authenticator, just fail it. 
570         ClientAuthenticator authenticator = getSaslAuthenticator(); 
571         if (authenticator == null) {
572             throw new MessagingException("Unable to obtain SASL authenticator"); 
573         }
574         
575         // go process the login.
576         return processLogin(authenticator);
577     }
578     
579     /**
580      * Attempt to retrieve a SASL authenticator for this 
581      * protocol. 
582      * 
583      * @return A SASL authenticator, or null if a suitable one 
584      *         was not located.
585      */
586     protected ClientAuthenticator getSaslAuthenticator() {
587         return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm); 
588     }
589 
590 
591     /**
592      * Process a login using the provided authenticator object.
593      * 
594      * NB:  This method is synchronized because we have a multi-step process going on 
595      * here.  No other commands should be sent to the server until we complete. 
596      *
597      * @return Returns true if the server support a SASL authentication mechanism and
598      * accepted reponse challenges.
599      * @exception MessagingException
600      */
601     protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException {
602         debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
603 
604         // if the authenticator has some initial data, we compose a command
605         // containing the initial data.
606         if (authenticator.hasInitialResponse()) {
607             StringBuffer command = new StringBuffer();
608             // the auth command initiates the handshaking.
609             command.append("AUTHINFO SASL ");
610             // and tell the server which mechanism we're using.
611             command.append(authenticator.getMechanismName());
612             command.append(" ");
613             // and append the response data
614             command.append(new String(Base64.encode(authenticator.evaluateChallenge(null))));
615             // send the command now
616             sendLine(command.toString());
617         }
618         // we just send an auth command with the command type.
619         else {
620             StringBuffer command = new StringBuffer();
621             // the auth command initiates the handshaking.
622             command.append("AUTHINFO SASL");
623             // and tell the server which mechanism we're using.
624             command.append(authenticator.getMechanismName());
625             // send the command now
626             sendLine(command.toString());
627         }
628 
629         // now process the challenge sequence. We get a 235 response back when
630         // the server accepts the
631         // authentication, and a 334 indicates we have an additional challenge.
632         while (true) {
633             // get the next line, and if it is an error response, return now.
634             NNTPReply line = getReply();
635 
636             // if we get a completion return, we've passed muster, so give an
637             // authentication response.
638             if (line.getCode() == NNTPReply.AUTHINFO_ACCEPTED || line.getCode() == NNTPReply.AUTHINFO_ACCEPTED_FINAL) {
639                 debugOut("Successful SMTP authentication");
640                 return true;
641             }
642             // we have an additional challenge to process.
643             else if (line.getCode() == NNTPReply.AUTHINFO_CHALLENGE) {
644                 // Does the authenticator think it is finished? We can't answer
645                 // an additional challenge,
646                 // so fail this.
647                 if (authenticator.isComplete()) {
648                     debugOut("Extra authentication challenge " + line);
649                     return false;
650                 }
651 
652                 // we're passed back a challenge value, Base64 encoded.
653                 byte[] challenge = Base64.decode(line.getMessage().getBytes());
654 
655                 // have the authenticator evaluate and send back the encoded
656                 // response.
657                 sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge))));
658             }
659             // completion or challenge are the only responses we know how to
660             // handle. Anything else must
661             // be a failure.
662             else {
663                 debugOut("Authentication failure " + line);
664                 return false;
665             }
666         }
667     }
668 
669     
670     /**
671      * Process an AUTHINFO USER command. Most common form of NNTP
672      * authentication.
673      * 
674      * @exception MessagingException
675      */
676     protected void processAuthinfoUser() throws MessagingException {
677         // only do this if allowed by the server 
678         if (!authInfoUserAllowed) {
679             return; 
680         }
681         NNTPReply reply = sendAuthCommand("AUTHINFO USER " + username);
682         // accepted without a password (uncommon, but allowed), we're done
683         if (reply.getCode() == NNTPReply.AUTHINFO_ACCEPTED) {
684             return;
685         }
686         // the only other non-error response is continue.
687         if (reply.getCode() != NNTPReply.AUTHINFO_CONTINUE) {
688             throw new MessagingException("Error authenticating with server using AUTHINFO USER: " + reply);
689         }
690         // now send the password. We expect an accepted response.
691         reply = sendAuthCommand("AUTHINFO PASS " + password);
692         if (reply.getCode() != NNTPReply.AUTHINFO_ACCEPTED) {
693             throw new MessagingException("Error authenticating with server using AUTHINFO SIMPLE");
694         }
695     }
696 
697     
698     /**
699      * Indicate whether posting is allowed for a given server.
700      * 
701      * @return True if the server allows posting, false if the server is
702      *         read-only.
703      */
704     public boolean isPostingAllowed() {
705         return postingAllowed;
706     }
707 
708     /**
709      * Retrieve the welcome string sent back from the server.
710      * 
711      * @return The server provided welcome string.
712      */
713     public String getWelcomeString() {
714         return welcomeString;
715     }
716 }