001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package org.apache.geronimo.javamail.transport.nntp;
021    
022    import java.io.BufferedReader;
023    import java.io.BufferedOutputStream; 
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.InputStreamReader;
027    import java.io.OutputStream;
028    import java.io.PrintStream;
029    import java.io.PrintWriter;
030    import java.lang.reflect.InvocationTargetException;
031    import java.lang.reflect.Method;
032    import java.net.InetAddress;
033    import java.net.Socket;
034    import java.util.ArrayList; 
035    import java.util.HashMap;
036    import java.util.List;
037    import java.util.StringTokenizer;
038    
039    import javax.mail.Message;
040    import javax.mail.MessagingException;
041    import javax.mail.Session;
042    
043    import org.apache.geronimo.javamail.authentication.ClientAuthenticator; 
044    import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 
045    import org.apache.geronimo.javamail.util.MailConnection; 
046    import org.apache.geronimo.javamail.util.MIMEOutputStream;
047    import org.apache.geronimo.javamail.util.ProtocolProperties; 
048    import org.apache.geronimo.mail.util.Base64;
049    import org.apache.geronimo.mail.util.SessionUtil;
050    
051    /**
052     * Simple implementation of NNTP transport. Just does plain RFC977-ish delivery.
053     * 
054     * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $
055     */
056    public class NNTPConnection extends MailConnection {
057    
058        /**
059         * constants for EOL termination
060         */
061        protected static final char CR = '\r';
062    
063        protected static final char LF = '\n';
064    
065        /**
066         * property keys for protocol properties.
067         */
068        protected static final int DEFAULT_NNTP_PORT = 119;
069        // does the server support posting?
070        protected boolean postingAllowed = true;
071        
072        // different authentication mechanisms 
073        protected boolean authInfoUserAllowed = false; 
074        protected boolean authInfoSaslAllowed = false; 
075        
076        // the last response line received from the server.
077        protected NNTPReply lastServerResponse = null;
078    
079        // the welcome string from the server.
080        protected String welcomeString = null;
081        
082        // input reader wrapped around the socket input stream 
083        protected BufferedReader reader; 
084        // output writer wrapped around the socket output stream. 
085        protected PrintWriter writer; 
086    
087        /**
088         * Normal constructor for an NNTPConnection() object.
089         * 
090         * @param props  The property bundle for this protocol instance.
091         */
092        public NNTPConnection(ProtocolProperties props) {
093            super(props);
094        }
095        
096        
097        /**
098         * Connect to the server and do the initial handshaking.
099         * 
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    }