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.smtp; 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.io.UnsupportedEncodingException; 031 import java.lang.reflect.InvocationTargetException; 032 import java.lang.reflect.Method; 033 import java.net.InetAddress; 034 import java.net.Socket; 035 import java.net.SocketException; 036 import java.util.ArrayList; 037 import java.util.HashMap; 038 import java.util.List; 039 import java.util.StringTokenizer; 040 041 import javax.mail.Address; 042 import javax.mail.AuthenticationFailedException; 043 import javax.mail.Message; 044 import javax.mail.MessagingException; 045 import javax.mail.internet.InternetAddress; 046 import javax.mail.internet.MimeMessage; 047 import javax.mail.internet.MimeMultipart; 048 import javax.mail.internet.MimePart; 049 import javax.mail.Session; 050 051 import org.apache.geronimo.javamail.authentication.ClientAuthenticator; 052 import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 053 import org.apache.geronimo.javamail.util.CountingOutputStream; 054 import org.apache.geronimo.javamail.util.MailConnection; 055 import org.apache.geronimo.javamail.util.MIMEOutputStream; 056 import org.apache.geronimo.javamail.util.ProtocolProperties; 057 import org.apache.geronimo.mail.util.Base64; 058 import org.apache.geronimo.mail.util.XText; 059 060 /** 061 * Simple implementation of SMTP transport. Just does plain RFC977-ish delivery. 062 * 063 * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $ 064 */ 065 public class SMTPConnection extends MailConnection { 066 protected static final String MAIL_SMTP_QUITWAIT = "quitwait"; 067 protected static final String MAIL_SMTP_EXTENSION = "mailextension"; 068 protected static final String MAIL_SMTP_EHLO = "ehlo"; 069 protected static final String MAIL_SMTP_ALLOW8BITMIME = "allow8bitmime"; 070 protected static final String MAIL_SMTP_REPORT_SUCCESS = "reportsuccess"; 071 protected static final String MAIL_SMTP_STARTTLS_ENABLE = "starttls.enable"; 072 protected static final String MAIL_SMTP_AUTH = "auth"; 073 protected static final String MAIL_SMTP_FROM = "from"; 074 protected static final String MAIL_SMTP_DSN_RET = "dsn.ret"; 075 protected static final String MAIL_SMTP_SUBMITTER = "submitter"; 076 077 /** 078 * property keys for protocol properties. 079 */ 080 protected static final int DEFAULT_NNTP_PORT = 119; 081 082 // the last response line received from the server. 083 protected SMTPReply lastServerResponse = null; 084 085 // input reader wrapped around the socket input stream 086 protected BufferedReader reader; 087 // output writer wrapped around the socket output stream. 088 protected PrintWriter writer; 089 090 // do we report success after completion of each mail send. 091 protected boolean reportSuccess; 092 // does the server support transport level security? 093 protected boolean serverTLS = false; 094 // is TLS enabled on our part? 095 protected boolean useTLS = false; 096 // should we use 8BITMIME encoding if supported by the server? 097 protected boolean use8bit = false; 098 099 /** 100 * Normal constructor for an SMTPConnection() object. 101 * 102 * @param props The property bundle for this protocol instance. 103 */ 104 public SMTPConnection(ProtocolProperties props) { 105 super(props); 106 107 // check to see if we need to throw an exception after a send operation. 108 reportSuccess = props.getBooleanProperty(MAIL_SMTP_REPORT_SUCCESS, false); 109 // and also check for TLS enablement. 110 useTLS = props.getBooleanProperty(MAIL_SMTP_STARTTLS_ENABLE, false); 111 // and also check for 8bitmime support 112 use8bit = props.getBooleanProperty(MAIL_SMTP_ALLOW8BITMIME, false); 113 } 114 115 116 /** 117 * Connect to the server and do the initial handshaking. 118 * 119 * @param host The target host name. 120 * @param port The target port 121 * @param username The connection username (can be null) 122 * @param password The authentication password (can be null). 123 * 124 * @return true if we were able to obtain a connection and 125 * authenticate. 126 * @exception MessagingException 127 */ 128 public boolean protocolConnect(String host, int port, String username, String password) throws MessagingException { 129 130 // now check to see if we need to authenticate. If we need this, then 131 // we must have a username and 132 // password specified. Failing this may result in a user prompt to 133 // collect the information. 134 boolean mustAuthenticate = props.getBooleanProperty(MAIL_SMTP_AUTH, false); 135 136 // if we need to authenticate, and we don't have both a userid and 137 // password, then we fail this 138 // immediately. The Service.connect() method will try to obtain the user 139 // information and retry the 140 // connection one time. 141 if (mustAuthenticate && (username == null || password == null)) { 142 debugOut("Failing connection for missing authentication information"); 143 return false; 144 } 145 146 super.protocolConnect(host, port, username, password); 147 148 try { 149 // create socket and connect to server. 150 getConnection(); 151 152 // receive welcoming message 153 if (!getWelcome()) { 154 debugOut("Error getting welcome message"); 155 throw new MessagingException("Error in getting welcome msg"); 156 } 157 158 // say hello 159 if (!sendHandshake()) { 160 debugOut("Error getting processing handshake message"); 161 throw new MessagingException("Error in saying EHLO to server"); 162 } 163 164 // authenticate with the server, if necessary 165 if (!processAuthentication()) { 166 debugOut("User authentication failure"); 167 throw new AuthenticationFailedException("Error authenticating with server"); 168 } 169 } catch (IOException e) { 170 debugOut("I/O exception establishing connection", e); 171 throw new MessagingException("Connection error", e); 172 } 173 debugOut("Successful connection"); 174 return true; 175 } 176 177 178 /** 179 * Close the connection. On completion, we'll be disconnected from the 180 * server and unable to send more data. 181 * 182 * @exception MessagingException 183 */ 184 public void close() throws MessagingException { 185 // if we're already closed, get outta here. 186 if (socket == null) { 187 return; 188 } 189 try { 190 // say goodbye 191 sendQuit(); 192 } finally { 193 // and close up the connection. We do this in a finally block to 194 // make sure the connection 195 // is shut down even if quit gets an error. 196 closeServerConnection(); 197 } 198 } 199 200 public String toString() { 201 return "SMTPConnection host: " + serverHost + " port: " + serverPort; 202 } 203 204 205 /** 206 * Set the sender for this mail. 207 * 208 * @param message 209 * The message we're sending. 210 * 211 * @return True if the command was accepted, false otherwise. 212 * @exception MessagingException 213 */ 214 protected boolean sendMailFrom(Message message) throws MessagingException { 215 216 // need to sort the from value out from a variety of sources. 217 String from = null; 218 219 // first potential source is from the message itself, if it's an 220 // instance of SMTPMessage. 221 if (message instanceof SMTPMessage) { 222 from = ((SMTPMessage) message).getEnvelopeFrom(); 223 } 224 225 // if not available from the message, check the protocol property next 226 if (from == null || from.length() == 0) { 227 // the from value can be set explicitly as a property 228 from = props.getProperty(MAIL_SMTP_FROM); 229 } 230 231 // if not there, see if we have something in the message header. 232 if (from == null || from.length() == 0) { 233 Address[] fromAddresses = message.getFrom(); 234 235 // if we have some addresses in the header, then take the first one 236 // as our From: address 237 if (fromAddresses != null && fromAddresses.length > 0) { 238 from = ((InternetAddress) fromAddresses[0]).getAddress(); 239 } 240 // get what the InternetAddress class believes to be the local 241 // address. 242 else { 243 InternetAddress local = InternetAddress.getLocalAddress(session); 244 if (local != null) { 245 from = local.getAddress(); 246 } 247 } 248 } 249 250 if (from == null || from.length() == 0) { 251 throw new MessagingException("no FROM address"); 252 } 253 254 StringBuffer command = new StringBuffer(); 255 256 // start building up the command 257 command.append("MAIL FROM: "); 258 command.append(fixEmailAddress(from)); 259 260 // If the server supports the 8BITMIME extension, we might need to change the 261 // transfer encoding for the content to allow for direct transmission of the 262 // 8-bit codes. 263 if (supportsExtension("8BITMIME")) { 264 // we only do this if the capability was enabled via a property option or 265 // by explicitly setting the property on the message object. 266 if (use8bit || (message instanceof SMTPMessage && ((SMTPMessage)message).getAllow8bitMIME())) { 267 // make sure we add the BODY= option to the FROM message. 268 command.append(" BODY=8BITMIME"); 269 270 // go check the content and see if the can convert the transfer encoding to 271 // allow direct 8-bit transmission. 272 if (convertTransferEncoding((MimeMessage)message)) { 273 // if we changed the encoding on any of the parts, then we 274 // need to save the message again 275 message.saveChanges(); 276 } 277 } 278 } 279 280 // some servers ask for a size estimate on the initial send 281 if (supportsExtension("SIZE")) { 282 int estimate = getSizeEstimate(message); 283 if (estimate > 0) { 284 command.append(" SIZE=" + estimate); 285 } 286 } 287 288 // does this server support Delivery Status Notification? Then we may 289 // need to add some extra to the command. 290 if (supportsExtension("DSN")) { 291 String returnNotification = null; 292 293 // the return notification stuff might be set as value on the 294 // message object itself. 295 if (message instanceof SMTPMessage) { 296 // we need to convert the option into a string value. 297 switch (((SMTPMessage) message).getReturnOption()) { 298 case SMTPMessage.RETURN_FULL: 299 returnNotification = "FULL"; 300 break; 301 302 case SMTPMessage.RETURN_HDRS: 303 returnNotification = "HDRS"; 304 break; 305 } 306 } 307 308 // if not obtained from the message object, it can also be set as a 309 // property. 310 if (returnNotification == null) { 311 // the DSN value is set by yet another property. 312 returnNotification = props.getProperty(MAIL_SMTP_DSN_RET); 313 } 314 315 // if we have a target, add the notification stuff to our FROM 316 // command. 317 if (returnNotification != null) { 318 command.append(" RET="); 319 command.append(returnNotification); 320 } 321 } 322 323 // if this server supports AUTH and we have submitter information, then 324 // we also add the 325 // "AUTH=" keyword to the MAIL FROM command (see RFC 2554). 326 327 if (supportsExtension("AUTH")) { 328 String submitter = null; 329 330 // another option that can be specified on the message object. 331 if (message instanceof SMTPMessage) { 332 submitter = ((SMTPMessage) message).getSubmitter(); 333 } 334 // if not part of the object, try for a propery version. 335 if (submitter == null) { 336 // we only send the extra keyword is a submitter is specified. 337 submitter = props.getProperty(MAIL_SMTP_SUBMITTER); 338 } 339 // we have one...add the keyword, plus the submitter info in xtext 340 // format (defined by RFC 1891). 341 if (submitter != null) { 342 command.append(" AUTH="); 343 try { 344 // add this encoded 345 command.append(new String(XText.encode(submitter.getBytes("US-ASCII")))); 346 } catch (UnsupportedEncodingException e) { 347 throw new MessagingException("Invalid submitter value " + submitter); 348 } 349 } 350 } 351 352 String extension = null; 353 354 // now see if we need to add any additional extension info to this 355 // command. The extension is not 356 // checked for validity. That's the reponsibility of the caller. 357 if (message instanceof SMTPMessage) { 358 extension = ((SMTPMessage) message).getMailExtension(); 359 } 360 // this can come either from the object or from a set property. 361 if (extension == null) { 362 extension = props.getProperty(MAIL_SMTP_EXTENSION); 363 } 364 365 // have something real to add? 366 if (extension != null && extension.length() != 0) { 367 // tack this on the end with a blank delimiter. 368 command.append(' '); 369 command.append(extension); 370 } 371 372 // and finally send the command 373 SMTPReply line = sendCommand(command.toString()); 374 375 // 250 response indicates success. 376 return line.getCode() == SMTPReply.COMMAND_ACCEPTED; 377 } 378 379 380 /** 381 * Check to see if a MIME body part can have its 382 * encoding changed from quoted-printable or base64 383 * encoding to 8bit encoding. In order for this 384 * to work, it must follow the rules laid out in 385 * RFC 2045. To qualify for conversion, the text 386 * must be: 387 * 388 * 1) No more than 998 bytes long 389 * 2) All lines are terminated with CRLF sequences 390 * 3) CR and LF characters only occur in properly 391 * formed line separators 392 * 4) No null characters are allowed. 393 * 394 * The conversion will only be applied to text 395 * elements, and this will recurse through the 396 * different elements of MultiPart content. 397 * 398 * @param bodyPart The bodyPart to convert. Initially, this will be 399 * the message itself. 400 * 401 * @return true if any conversion was performed, false if 402 * nothing was converted. 403 */ 404 protected boolean convertTransferEncoding(MimePart bodyPart) 405 { 406 boolean converted = false; 407 try { 408 // if this is a multipart element, apply the conversion rules 409 // to each of the parts. 410 if (bodyPart.isMimeType("multipart/")) { 411 MimeMultipart parts = (MimeMultipart)bodyPart.getContent(); 412 for (int i = 0; i < parts.getCount(); i++) { 413 // convert each body part, and accumulate the conversion result 414 converted = converted && convertTransferEncoding((MimePart)parts.getBodyPart(i)); 415 } 416 } 417 else { 418 // we only do this if the encoding is quoted-printable or base64 419 String encoding = bodyPart.getEncoding(); 420 if (encoding != null) { 421 encoding = encoding.toLowerCase(); 422 if (encoding.equals("quoted-printable") || encoding.equals("base64")) { 423 // this requires encoding. Read the actual content to see if 424 // it conforms to the 8bit encoding rules. 425 if (isValid8bit(bodyPart.getInputStream())) { 426 // There's a huge hidden gotcha lurking under the covers here. 427 // If the content just exists as an encoded byte array, then just 428 // switching the transfer encoding will mess things up because the 429 // already encoded data gets transmitted in encoded form, but with 430 // and 8bit encoding style. As a result, it doesn't get unencoded on 431 // the receiving end. This is a nasty problem to debug. 432 // 433 // The solution is to get the content as it's object type, set it back 434 // on the the message in raw form. Requesting the content will apply the 435 // current transfer encoding value to the data. Once we have set the 436 // content value back, we can reset the transfer encoding. 437 bodyPart.setContent(bodyPart.getContent(), bodyPart.getContentType()); 438 439 // it's valid, so change the transfer encoding to just 440 // pass the data through. 441 bodyPart.setHeader("Content-Transfer-Encoding", "8bit"); 442 converted = true; // we've changed something 443 } 444 } 445 } 446 } 447 } catch (MessagingException e) { 448 } catch (IOException e) { 449 } 450 return converted; 451 } 452 453 454 /** 455 * Get the server's welcome blob from the wire.... 456 */ 457 protected boolean getWelcome() throws MessagingException { 458 SMTPReply line = getReply(); 459 // just return the error status...we don't care about any of the 460 // response information 461 return !line.isError(); 462 } 463 464 465 /** 466 * Get an estimate of the transmission size for this 467 * message. This size is the complete message as it is 468 * encoded and transmitted on the DATA command, not counting 469 * the terminating ".CRLF". 470 * 471 * @param msg The message we're sending. 472 * 473 * @return The count of bytes, if it can be calculated. 474 */ 475 protected int getSizeEstimate(Message msg) { 476 // now the data... I could look at the type, but 477 try { 478 CountingOutputStream outputStream = new CountingOutputStream(); 479 480 // the data content has two requirements we need to meet by 481 // filtering the 482 // output stream. Requirement 1 is to conicalize any line breaks. 483 // All line 484 // breaks will be transformed into properly formed CRLF sequences. 485 // 486 // Requirement 2 is to perform byte-stuff for any line that begins 487 // with a "." 488 // so that data is not confused with the end-of-data marker (a 489 // "\r\n.\r\n" sequence. 490 // 491 // The MIME output stream performs those two functions on behalf of 492 // the content 493 // writer. 494 MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream); 495 496 msg.writeTo(mimeOut); 497 498 // now to finish, we make sure there's a line break at the end. 499 mimeOut.forceTerminatingLineBreak(); 500 // and flush the data to send it along 501 mimeOut.flush(); 502 503 return outputStream.getCount(); 504 } catch (IOException e) { 505 return 0; // can't get an estimate 506 } catch (MessagingException e) { 507 return 0; // can't get an estimate 508 } 509 } 510 511 512 /** 513 * Sends the data in the message down the socket. This presumes the server 514 * is in the right place and ready for getting the DATA message and the data 515 * right place in the sequence 516 */ 517 protected void sendData(Message msg) throws MessagingException { 518 519 // send the DATA command 520 SMTPReply line = sendCommand("DATA"); 521 522 if (line.isError()) { 523 throw new MessagingException("Error issuing SMTP 'DATA' command: " + line); 524 } 525 526 // now the data... I could look at the type, but 527 try { 528 // the data content has two requirements we need to meet by 529 // filtering the 530 // output stream. Requirement 1 is to conicalize any line breaks. 531 // All line 532 // breaks will be transformed into properly formed CRLF sequences. 533 // 534 // Requirement 2 is to perform byte-stuff for any line that begins 535 // with a "." 536 // so that data is not confused with the end-of-data marker (a 537 // "\r\n.\r\n" sequence. 538 // 539 // The MIME output stream performs those two functions on behalf of 540 // the content 541 // writer. 542 MIMEOutputStream mimeOut = new MIMEOutputStream(outputStream); 543 544 msg.writeTo(mimeOut); 545 546 // now to finish, we send a CRLF sequence, followed by a ".". 547 mimeOut.writeSMTPTerminator(); 548 // and flush the data to send it along 549 mimeOut.flush(); 550 } catch (IOException e) { 551 throw new MessagingException(e.toString()); 552 } catch (MessagingException e) { 553 throw new MessagingException(e.toString()); 554 } 555 556 // use a longer time out here to give the server time to process the 557 // data. 558 try { 559 line = new SMTPReply(receiveLine(TIMEOUT * 2)); 560 } catch (MalformedSMTPReplyException e) { 561 throw new MessagingException(e.toString()); 562 } catch (MessagingException e) { 563 throw new MessagingException(e.toString()); 564 } 565 566 if (line.isError()) { 567 throw new MessagingException("Error issuing SMTP 'DATA' command: " + line); 568 } 569 } 570 571 /** 572 * Sends the QUIT message and receieves the response 573 */ 574 protected void sendQuit() throws MessagingException { 575 // there's yet another property that controls whether we should wait for 576 // a reply for a QUIT command. If true, we're suppposed to wait for a response 577 // from the QUIT command. Otherwise we just send the QUIT and bail. The default 578 // is "false" 579 if (props.getBooleanProperty(MAIL_SMTP_QUITWAIT, true)) { 580 // handle as a real command...we're going to ignore the response. 581 sendCommand("QUIT"); 582 } else { 583 // just send the command without waiting for a response. 584 sendLine("QUIT"); 585 } 586 } 587 588 /** 589 * Sets a receiver address for the current message 590 * 591 * @param addr 592 * The target address. 593 * @param dsn 594 * An optional DSN option appended to the RCPT TO command. 595 * 596 * @return The status for this particular send operation. 597 * @exception MessagingException 598 */ 599 public SendStatus sendRcptTo(InternetAddress addr, String dsn) throws MessagingException { 600 // compose the command using the fixed up email address. Normally, this 601 // involves adding 602 // "<" and ">" around the address. 603 604 StringBuffer command = new StringBuffer(); 605 606 // compose the first part of the command 607 command.append("RCPT TO: "); 608 command.append(fixEmailAddress(addr.getAddress())); 609 610 // if we have DSN information, append it to the command. 611 if (dsn != null) { 612 command.append(" NOTIFY="); 613 command.append(dsn); 614 } 615 616 // get a string version of this command. 617 String commandString = command.toString(); 618 619 SMTPReply line = sendCommand(commandString); 620 621 switch (line.getCode()) { 622 // these two are both successful transmissions 623 case SMTPReply.COMMAND_ACCEPTED: 624 case SMTPReply.ADDRESS_NOT_LOCAL: 625 // we get out of here with the status information. 626 return new SendStatus(SendStatus.SUCCESS, addr, commandString, line); 627 628 // these are considered invalid address errors 629 case SMTPReply.PARAMETER_SYNTAX_ERROR: 630 case SMTPReply.INVALID_COMMAND_SEQUENCE: 631 case SMTPReply.MAILBOX_NOT_FOUND: 632 case SMTPReply.INVALID_MAILBOX: 633 case SMTPReply.USER_NOT_LOCAL: 634 // we get out of here with the status information. 635 return new SendStatus(SendStatus.INVALID_ADDRESS, addr, commandString, line); 636 637 // the command was valid, but something went wrong in the server. 638 case SMTPReply.SERVICE_NOT_AVAILABLE: 639 case SMTPReply.MAILBOX_BUSY: 640 case SMTPReply.PROCESSING_ERROR: 641 case SMTPReply.INSUFFICIENT_STORAGE: 642 case SMTPReply.MAILBOX_FULL: 643 // we get out of here with the status information. 644 return new SendStatus(SendStatus.SEND_FAILURE, addr, commandString, line); 645 646 // everything else is considered really bad... 647 default: 648 // we get out of here with the status information. 649 return new SendStatus(SendStatus.GENERAL_ERROR, addr, commandString, line); 650 } 651 } 652 653 /** 654 * Send a command to the server, returning the first response line back as a 655 * reply. 656 * 657 * @param data 658 * The data to send. 659 * 660 * @return A reply object with the reply line. 661 * @exception MessagingException 662 */ 663 protected SMTPReply sendCommand(String data) throws MessagingException { 664 sendLine(data); 665 return getReply(); 666 } 667 668 /** 669 * Sends a message down the socket and terminates with the appropriate CRLF 670 */ 671 protected void sendLine(String data) throws MessagingException { 672 if (socket == null || !socket.isConnected()) { 673 throw new MessagingException("no connection"); 674 } 675 try { 676 outputStream.write(data.getBytes()); 677 outputStream.write(CR); 678 outputStream.write(LF); 679 outputStream.flush(); 680 } catch (IOException e) { 681 throw new MessagingException(e.toString()); 682 } 683 } 684 685 /** 686 * Receives one line from the server. A line is a sequence of bytes 687 * terminated by a CRLF 688 * 689 * @return the line from the server as String 690 */ 691 protected String receiveLine() throws MessagingException { 692 return receiveLine(TIMEOUT); 693 } 694 695 /** 696 * Get a reply line for an SMTP command. 697 * 698 * @return An SMTP reply object from the stream. 699 */ 700 protected SMTPReply getReply() throws MessagingException { 701 try { 702 lastServerResponse = new SMTPReply(receiveLine()); 703 // if the first line we receive is a continuation, continue 704 // reading lines until we reach the non-continued one. 705 while (lastServerResponse.isContinued()) { 706 lastServerResponse.addLine(receiveLine()); 707 } 708 } catch (MalformedSMTPReplyException e) { 709 throw new MessagingException(e.toString()); 710 } catch (MessagingException e) { 711 throw e; 712 } 713 return lastServerResponse; 714 } 715 716 /** 717 * Retrieve the last response received from the SMTP server. 718 * 719 * @return The raw response string (including the error code) returned from 720 * the SMTP server. 721 */ 722 public SMTPReply getLastServerResponse() { 723 return lastServerResponse; 724 } 725 726 727 /** 728 * Receives one line from the server. A line is a sequence of bytes 729 * terminated by a CRLF 730 * 731 * @return the line from the server as String 732 */ 733 protected String receiveLine(int delayMillis) throws MessagingException { 734 if (socket == null || !socket.isConnected()) { 735 throw new MessagingException("no connection"); 736 } 737 738 int timeout = 0; 739 740 try { 741 // for now, read byte for byte, looking for a CRLF 742 timeout = socket.getSoTimeout(); 743 744 socket.setSoTimeout(delayMillis); 745 746 StringBuffer buff = new StringBuffer(); 747 748 int c; 749 boolean crFound = false, lfFound = false; 750 751 while ((c = inputStream.read()) != -1 && crFound == false && lfFound == false) { 752 // we're looking for a CRLF sequence, so mark each one as seen. 753 // Any other 754 // character gets appended to the end of the buffer. 755 if (c == CR) { 756 crFound = true; 757 } else if (c == LF) { 758 lfFound = true; 759 } else { 760 buff.append((char) c); 761 } 762 } 763 764 String line = buff.toString(); 765 return line; 766 767 } catch (SocketException e) { 768 throw new MessagingException(e.toString()); 769 } catch (IOException e) { 770 throw new MessagingException(e.toString()); 771 } finally { 772 try { 773 socket.setSoTimeout(timeout); 774 } catch (SocketException e) { 775 // ignore - was just trying to do the decent thing... 776 } 777 } 778 } 779 780 /** 781 * Convert an InternetAddress into a form sendable on an SMTP mail command. 782 * InternetAddress.getAddress() generally returns just the address portion 783 * of the full address, minus route address markers. We need to ensure we 784 * have an address with '<' and '>' delimiters. 785 * 786 * @param mail 787 * The mail address returned from InternetAddress.getAddress(). 788 * 789 * @return A string formatted for sending. 790 */ 791 protected String fixEmailAddress(String mail) { 792 if (mail.charAt(0) == '<') { 793 return mail; 794 } 795 return "<" + mail + ">"; 796 } 797 798 /** 799 * Start the handshake process with the server, including setting up and 800 * TLS-level work. At the completion of this task, we should be ready to 801 * authenticate with the server, if needed. 802 */ 803 protected boolean sendHandshake() throws MessagingException { 804 // check to see what sort of initial handshake we need to make. 805 boolean useEhlo = props.getBooleanProperty(MAIL_SMTP_EHLO, true); 806 // if we're to use Ehlo, send it and then fall back to just a HELO 807 // message if it fails. 808 if (useEhlo) { 809 if (!sendEhlo()) { 810 sendHelo(); 811 } 812 } else { 813 // send the initial hello response. 814 sendHelo(); 815 } 816 817 if (useTLS) { 818 // if we've been told to use TLS, and this server doesn't support 819 // it, then this is a failure 820 if (!serverTLS) { 821 throw new MessagingException("Server doesn't support required transport level security"); 822 } 823 // if the server supports TLS, then use it for the connection. 824 // on our connection. 825 getConnectedTLSSocket(); 826 827 // some servers (gmail is one that I know of) only send a STARTTLS 828 // extension message on the 829 // first EHLO command. Now that we have the TLS handshaking 830 // established, we need to send a 831 // second EHLO message to retrieve the AUTH records from the server. 832 if (!sendEhlo()) { 833 throw new MessagingException("Failure sending EHLO command to SMTP server"); 834 } 835 } 836 837 // this worked. 838 return true; 839 } 840 841 842 /** 843 * Switch the connection to using TLS level security, switching to an SSL 844 * socket. 845 */ 846 protected void getConnectedTLSSocket() throws MessagingException { 847 debugOut("Attempting to negotiate STARTTLS with server " + serverHost); 848 // tell the server of our intention to start a TLS session 849 SMTPReply line = sendCommand("STARTTLS"); 850 851 if (line.getCode() != SMTPReply.SERVICE_READY) { 852 debugOut("STARTTLS command rejected by SMTP server " + serverHost); 853 throw new MessagingException("Unable to make TLS server connection"); 854 } 855 856 debugOut("STARTTLS command accepted"); 857 858 // the base class handles the socket switch details 859 super.getConnectedTLSSocket(); 860 } 861 862 863 /** 864 * Send the EHLO command to the SMTP server. 865 * 866 * @return True if the command was accepted ok, false for any errors. 867 * @exception SMTPTransportException 868 * @exception MalformedSMTPReplyException 869 * @exception MessagingException 870 */ 871 protected boolean sendEhlo() throws MessagingException { 872 sendLine("EHLO " + getLocalHost()); 873 874 SMTPReply reply = getReply(); 875 876 // we get a 250 code back. The first line is just a greeting, and 877 // extensions are identifed on 878 // continuations. If this fails, then we'll try once more with HELO to 879 // establish bona fides. 880 if (reply.getCode() != SMTPReply.COMMAND_ACCEPTED) { 881 return false; 882 } 883 884 // create a fresh mapping and authentications table 885 capabilities = new HashMap(); 886 authentications = new ArrayList(); 887 888 List lines = reply.getLines(); 889 // process all of the continuation lines 890 for (int i = 1; i < lines.size(); i++) { 891 // go process the extention 892 processExtension((String)lines.get(i)); 893 } 894 return true; 895 } 896 897 /** 898 * Send the HELO command to the SMTP server. 899 * 900 * @exception MessagingException 901 */ 902 protected void sendHelo() throws MessagingException { 903 // create a fresh mapping and authentications table 904 // these will be empty, but it will prevent NPEs 905 capabilities = new HashMap(); 906 authentications = new ArrayList(); 907 908 sendLine("HELO " + getLocalHost()); 909 910 SMTPReply line = getReply(); 911 912 // we get a 250 code back. The first line is just a greeting, and 913 // extensions are identifed on 914 // continuations. If this fails, then we'll try once more with HELO to 915 // establish bona fides. 916 if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) { 917 throw new MessagingException("Failure sending HELO command to SMTP server"); 918 } 919 } 920 921 /** 922 * Return the current startTLS property. 923 * 924 * @return The current startTLS property. 925 */ 926 public boolean getStartTLS() { 927 return useTLS; 928 } 929 930 931 /** 932 * Set a new value for the startTLS property. 933 * 934 * @param start 935 * The new setting. 936 */ 937 public void setStartTLS(boolean start) { 938 useTLS = start; 939 } 940 941 942 /** 943 * Process an extension string passed back as the EHLP response. 944 * 945 * @param extension 946 * The string value of the extension (which will be of the form 947 * "NAME arguments"). 948 */ 949 protected void processExtension(String extension) { 950 debugOut("Processing extension " + extension); 951 String extensionName = extension.toUpperCase(); 952 String argument = ""; 953 954 int delimiter = extension.indexOf(' '); 955 // if we have a keyword with arguments, parse them out and add to the 956 // argument map. 957 if (delimiter != -1) { 958 extensionName = extension.substring(0, delimiter).toUpperCase(); 959 argument = extension.substring(delimiter + 1); 960 } 961 962 // add this to the map so it can be tested later. 963 capabilities.put(extensionName, argument); 964 965 // process a few special ones that don't require extra parsing. 966 // AUTH and AUTH=LOGIN are handled the same 967 if (extensionName.equals("AUTH")) { 968 // if we don't have an argument on AUTH, this means LOGIN. 969 if (argument == null) { 970 authentications.add("LOGIN"); 971 } else { 972 // The security mechanisms are blank delimited tokens. 973 StringTokenizer tokenizer = new StringTokenizer(argument); 974 975 while (tokenizer.hasMoreTokens()) { 976 String mechanism = tokenizer.nextToken().toUpperCase(); 977 authentications.add(mechanism); 978 } 979 } 980 } 981 // special case for some older servers. 982 else if (extensionName.equals("AUTH=LOGIN")) { 983 authentications.add("LOGIN"); 984 } 985 // does this support transport level security? 986 else if (extensionName.equals("STARTTLS")) { 987 // flag this for later 988 serverTLS = true; 989 } 990 } 991 992 993 /** 994 * Retrieve any argument information associated with a extension reported 995 * back by the server on the EHLO command. 996 * 997 * @param name 998 * The name of the target server extension. 999 * 1000 * @return Any argument passed on a server extension. Returns null if the 1001 * extension did not include an argument or the extension was not 1002 * supported. 1003 */ 1004 public String extensionParameter(String name) { 1005 if (capabilities != null) { 1006 return (String)capabilities.get(name); 1007 } 1008 return null; 1009 } 1010 1011 1012 /** 1013 * Tests whether the target server supports a named extension. 1014 * 1015 * @param name 1016 * The target extension name. 1017 * 1018 * @return true if the target server reported on the EHLO command that is 1019 * supports the targer server, false if the extension was not 1020 * supported. 1021 */ 1022 public boolean supportsExtension(String name) { 1023 // this only returns null if we don't have this extension 1024 return extensionParameter(name) != null; 1025 } 1026 1027 1028 /** 1029 * Authenticate with the server, if necessary (or possible). 1030 * 1031 * @return true if we are ok to proceed, false for an authentication 1032 * failures. 1033 */ 1034 protected boolean processAuthentication() throws MessagingException { 1035 // no authentication defined? 1036 if (!props.getBooleanProperty(MAIL_SMTP_AUTH, false)) { 1037 return true; 1038 } 1039 1040 // we need to authenticate, but we don't have userid/password 1041 // information...fail this 1042 // immediately. 1043 if (username == null || password == null) { 1044 return false; 1045 } 1046 1047 // if unable to get an appropriate authenticator, just fail it. 1048 ClientAuthenticator authenticator = getSaslAuthenticator(); 1049 if (authenticator == null) { 1050 throw new MessagingException("Unable to obtain SASL authenticator"); 1051 } 1052 1053 1054 if (debug) { 1055 debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName()); 1056 } 1057 1058 // if the authenticator has some initial data, we compose a command 1059 // containing the initial data. 1060 if (authenticator.hasInitialResponse()) { 1061 StringBuffer command = new StringBuffer(); 1062 // the auth command initiates the handshaking. 1063 command.append("AUTH "); 1064 // and tell the server which mechanism we're using. 1065 command.append(authenticator.getMechanismName()); 1066 command.append(" "); 1067 // and append the response data 1068 command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)))); 1069 // send the command now 1070 sendLine(command.toString()); 1071 } 1072 // we just send an auth command with the command type. 1073 else { 1074 StringBuffer command = new StringBuffer(); 1075 // the auth command initiates the handshaking. 1076 command.append("AUTH "); 1077 // and tell the server which mechanism we're using. 1078 command.append(authenticator.getMechanismName()); 1079 // send the command now 1080 sendLine(command.toString()); 1081 } 1082 1083 // now process the challenge sequence. We get a 235 response back when 1084 // the server accepts the 1085 // authentication, and a 334 indicates we have an additional challenge. 1086 while (true) { 1087 // get the next line, and if it is an error response, return now. 1088 SMTPReply line; 1089 try { 1090 line = new SMTPReply(receiveLine()); 1091 } catch (MalformedSMTPReplyException e) { 1092 throw new MessagingException(e.toString()); 1093 } catch (MessagingException e) { 1094 throw e; 1095 } 1096 1097 // if we get a completion return, we've passed muster, so give an 1098 // authentication response. 1099 if (line.getCode() == SMTPReply.AUTHENTICATION_COMPLETE) { 1100 debugOut("Successful SMTP authentication"); 1101 return true; 1102 } 1103 // we have an additional challenge to process. 1104 else if (line.getCode() == SMTPReply.AUTHENTICATION_CHALLENGE) { 1105 // Does the authenticator think it is finished? We can't answer 1106 // an additional challenge, 1107 // so fail this. 1108 if (authenticator.isComplete()) { 1109 return false; 1110 } 1111 1112 // we're passed back a challenge value, Base64 encoded. 1113 byte[] challenge = Base64.decode(line.getMessage().getBytes()); 1114 1115 // have the authenticator evaluate and send back the encoded 1116 // response. 1117 sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)))); 1118 } 1119 // completion or challenge are the only responses we know how to 1120 // handle. Anything else must 1121 // be a failure. 1122 else { 1123 if (debug) { 1124 debugOut("Authentication failure " + line); 1125 } 1126 return false; 1127 } 1128 } 1129 } 1130 1131 1132 /** 1133 * Attempt to retrieve a SASL authenticator for this 1134 * protocol. 1135 * 1136 * @return A SASL authenticator, or null if a suitable one 1137 * was not located. 1138 */ 1139 protected ClientAuthenticator getSaslAuthenticator() { 1140 return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm); 1141 } 1142 1143 1144 /** 1145 * Read the bytes in a stream a test to see if this 1146 * conforms to the RFC 2045 rules for 8bit encoding. 1147 * 1148 * 1) No more than 998 bytes long 1149 * 2) All lines are terminated with CRLF sequences 1150 * 3) CR and LF characters only occur in properly 1151 * formed line separators 1152 * 4) No null characters are allowed. 1153 * 1154 * @param inStream The source input stream. 1155 * 1156 * @return true if this can be transmitted successfully 1157 * using 8bit encoding, false if an alternate encoding 1158 * will be required. 1159 */ 1160 protected boolean isValid8bit(InputStream inStream) { 1161 try { 1162 int ch; 1163 int lineLength = 0; 1164 while ((ch = inStream.read()) >= 0) { 1165 // nulls are decidedly not allowed 1166 if (ch == 0) { 1167 return false; 1168 } 1169 // start of a CRLF sequence (potentially) 1170 else if (ch == '\r') { 1171 // check the next character. There must be one, 1172 // and it must be a LF for this to be value 1173 ch = inStream.read(); 1174 if (ch != '\n') { 1175 return false; 1176 } 1177 // reset the line length 1178 lineLength = 0; 1179 } 1180 else { 1181 // a normal character 1182 lineLength++; 1183 // make sure the line is not too long 1184 if (lineLength > 998) { 1185 return false; 1186 } 1187 } 1188 1189 } 1190 } catch (IOException e) { 1191 return false; // can't read this, don't try passing it 1192 } 1193 // this converted ok 1194 return true; 1195 } 1196 1197 1198 /** 1199 * Simple holder class for the address/send status duple, as we can have 1200 * mixed success for a set of addresses and a message 1201 */ 1202 static public class SendStatus { 1203 public final static int SUCCESS = 0; 1204 1205 public final static int INVALID_ADDRESS = 1; 1206 1207 public final static int SEND_FAILURE = 2; 1208 1209 public final static int GENERAL_ERROR = 3; 1210 1211 // the status type of the send operation. 1212 int status; 1213 1214 // the address associated with this status 1215 InternetAddress address; 1216 1217 // the command string send to the server. 1218 String cmd; 1219 1220 // the reply from the server. 1221 SMTPReply reply; 1222 1223 /** 1224 * Constructor for a SendStatus item. 1225 * 1226 * @param s 1227 * The status type. 1228 * @param a 1229 * The address this is the status for. 1230 * @param c 1231 * The command string associated with this status. 1232 * @param r 1233 * The reply information from the server. 1234 */ 1235 public SendStatus(int s, InternetAddress a, String c, SMTPReply r) { 1236 this.cmd = c; 1237 this.status = s; 1238 this.address = a; 1239 this.reply = r; 1240 } 1241 1242 /** 1243 * Get the status information for this item. 1244 * 1245 * @return The current status code. 1246 */ 1247 public int getStatus() { 1248 return this.status; 1249 } 1250 1251 /** 1252 * Retrieve the InternetAddress object associated with this send 1253 * operation. 1254 * 1255 * @return The associated address object. 1256 */ 1257 public InternetAddress getAddress() { 1258 return this.address; 1259 } 1260 1261 /** 1262 * Retrieve the reply information associated with this send operati 1263 * 1264 * @return The SMTPReply object received for the operation. 1265 */ 1266 public SMTPReply getReply() { 1267 return reply; 1268 } 1269 1270 /** 1271 * Get the command string sent for this send operation. 1272 * 1273 * @return The command string for the MAIL TO command sent to the 1274 * server. 1275 */ 1276 public String getCommand() { 1277 return cmd; 1278 } 1279 1280 /** 1281 * Get an exception object associated with this send operation. There is 1282 * a mechanism for reporting send success via a send operation, so this 1283 * will be either a success or failure exception. 1284 * 1285 * @param reportSuccess 1286 * Indicates if we want success operations too. 1287 * 1288 * @return A newly constructed exception object. 1289 */ 1290 public MessagingException getException(boolean reportSuccess) { 1291 if (status != SUCCESS) { 1292 return new SMTPAddressFailedException(address, cmd, reply.getCode(), reply.getMessage()); 1293 } else { 1294 if (reportSuccess) { 1295 return new SMTPAddressSucceededException(address, cmd, reply.getCode(), reply.getMessage()); 1296 } 1297 } 1298 return null; 1299 } 1300 } 1301 1302 1303 /** 1304 * Reset the server connection after an error. 1305 * 1306 * @exception MessagingException 1307 */ 1308 public void resetConnection() throws MessagingException { 1309 // we want the caller to retrieve the last response responsbile for 1310 // requiring the reset, so save and 1311 // restore that info around the reset. 1312 SMTPReply last = lastServerResponse; 1313 1314 // send a reset command. 1315 SMTPReply line = sendCommand("RSET"); 1316 1317 // if this did not reset ok, just close the connection 1318 if (line.getCode() != SMTPReply.COMMAND_ACCEPTED) { 1319 close(); 1320 } 1321 // restore this. 1322 lastServerResponse = last; 1323 } 1324 1325 1326 /** 1327 * Return the current reportSuccess property. 1328 * 1329 * @return The current reportSuccess property. 1330 */ 1331 public boolean getReportSuccess() { 1332 return reportSuccess; 1333 } 1334 1335 /** 1336 * Set a new value for the reportSuccess property. 1337 * 1338 * @param report 1339 * The new setting. 1340 */ 1341 public void setReportSuccess(boolean report) { 1342 reportSuccess = report; 1343 } 1344 } 1345