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 }