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 }