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.IOException; 023 import java.io.UnsupportedEncodingException; 024 import java.net.Socket; 025 import java.util.ArrayList; 026 027 import javax.mail.Address; 028 import javax.mail.AuthenticationFailedException; 029 import javax.mail.Message; 030 import javax.mail.MessagingException; 031 import javax.mail.Session; 032 import javax.mail.Transport; 033 import javax.mail.URLName; 034 import javax.mail.event.TransportEvent; 035 import javax.mail.internet.InternetAddress; 036 import javax.mail.internet.MimeMessage; 037 import javax.mail.internet.MimeMultipart; 038 import javax.mail.internet.MimePart; 039 040 import org.apache.geronimo.javamail.util.ProtocolProperties; 041 import org.apache.geronimo.javamail.transport.smtp.SMTPConnection.SendStatus; 042 043 /** 044 * Simple implementation of SMTP transport. Just does plain RFC821-ish delivery. 045 * <p/> Supported properties : <p/> 046 * <ul> 047 * <li> mail.host : to set the server to deliver to. Default = localhost</li> 048 * <li> mail.smtp.port : to set the port. Default = 25</li> 049 * <li> mail.smtp.locahost : name to use for HELO/EHLO - default getHostName()</li> 050 * </ul> 051 * <p/> There is no way to indicate failure for a given recipient (it's possible 052 * to have a recipient address rejected). The sun impl throws exceptions even if 053 * others successful), but maybe we do a different way... <p/> TODO : lots. 054 * ESMTP, user/pass, indicate failure, etc... 055 * 056 * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $ 057 */ 058 public class SMTPTransport extends Transport { 059 /** 060 * property keys for protocol properties. The actual property name will be 061 * appended with "mail." + protocol + ".", where the protocol is either 062 * "smtp" or "smtps". 063 */ 064 protected static final String MAIL_SMTP_DSN_NOTIFY = "dsn.notify"; 065 protected static final String MAIL_SMTP_SENDPARTIAL = "sendpartial"; 066 protected static final String MAIL_SMTP_EXTENSION = "mailextension"; 067 protected static final String DEFAULT_MAIL_HOST = "localhost"; 068 069 protected static final int DEFAULT_MAIL_SMTP_PORT = 25; 070 protected static final int DEFAULT_MAIL_SMTPS_PORT = 465; 071 072 073 // do we use SSL for our initial connection? 074 protected boolean sslConnection = false; 075 076 // our accessor for protocol properties and the holder of 077 // protocol-specific information 078 protected ProtocolProperties props; 079 // our active connection object 080 protected SMTPConnection connection; 081 082 // the last response line received from the server. 083 protected SMTPReply lastServerResponse = null; 084 085 /** 086 * Normal constructor for an SMTPTransport() object. This constructor is 087 * used to build a transport instance for the "smtp" protocol. 088 * 089 * @param session 090 * The attached session. 091 * @param name 092 * An optional URLName object containing target information. 093 */ 094 public SMTPTransport(Session session, URLName name) { 095 this(session, name, "smtp", DEFAULT_MAIL_SMTP_PORT, false); 096 } 097 098 099 /** 100 * Common constructor used by the SMTPTransport and SMTPSTransport classes 101 * to do common initialization of defaults. 102 * 103 * @param session 104 * The host session instance. 105 * @param name 106 * The URLName of the target. 107 * @param protocol 108 * The protocol type (either "smtp" or "smtps". This helps us in 109 * retrieving protocol-specific session properties. 110 * @param defaultPort 111 * The default port used by this protocol. For "smtp", this will 112 * be 25. The default for "smtps" is 465. 113 * @param sslConnection 114 * Indicates whether an SSL connection should be used to initial 115 * contact the server. This is different from the STARTTLS 116 * support, which switches the connection to SSL after the 117 * initial startup. 118 */ 119 protected SMTPTransport(Session session, URLName name, String protocol, int defaultPort, boolean sslConnection) { 120 super(session, name); 121 122 // create the protocol property holder. This gives an abstraction over the different 123 // flavors of the protocol. 124 props = new ProtocolProperties(session, protocol, sslConnection, defaultPort); 125 // the connection manages connection for the transport 126 connection = new SMTPConnection(props); 127 } 128 129 130 /** 131 * Connect to a server using an already created socket. This connection is 132 * just like any other connection, except we will not create a new socket. 133 * 134 * @param socket 135 * The socket connection to use. 136 */ 137 public void connect(Socket socket) throws MessagingException { 138 connection.connect(socket); 139 super.connect(); 140 } 141 142 143 /** 144 * Do the protocol connection for an SMTP transport. This handles server 145 * authentication, if possible. Returns false if unable to connect to the 146 * server. 147 * 148 * @param host 149 * The target host name. 150 * @param port 151 * The server port number. 152 * @param user 153 * The authentication user (if any). 154 * @param password 155 * The server password. Might not be sent directly if more 156 * sophisticated authentication is used. 157 * 158 * @return true if we were able to connect to the server properly, false for 159 * any failures. 160 * @exception MessagingException 161 */ 162 protected boolean protocolConnect(String host, int port, String username, String password) 163 throws MessagingException { 164 // the connection pool handles all of the details here. 165 return connection.protocolConnect(host, port, username, password); 166 } 167 168 /** 169 * Send a message to multiple addressees. 170 * 171 * @param message 172 * The message we're sending. 173 * @param addresses 174 * An array of addresses to send to. 175 * 176 * @exception MessagingException 177 */ 178 public void sendMessage(Message message, Address[] addresses) throws MessagingException { 179 if (!isConnected()) { 180 throw new IllegalStateException("Not connected"); 181 } 182 // don't bother me w/ null messages or no addreses 183 if (message == null) { 184 throw new MessagingException("Null message"); 185 } 186 187 // SMTP only handles instances of MimeMessage, not the more general 188 // message case. 189 if (!(message instanceof MimeMessage)) { 190 throw new MessagingException("SMTP can only send MimeMessages"); 191 } 192 193 // we must have a message list. 194 if (addresses == null || addresses.length == 0) { 195 throw new MessagingException("Null or empty address array"); 196 } 197 198 boolean reportSuccess = getReportSuccess(); 199 200 // now see how we're configured for this send operation. 201 boolean partialSends = false; 202 203 // this can be attached directly to the message. 204 if (message instanceof SMTPMessage) { 205 partialSends = ((SMTPMessage) message).getSendPartial(); 206 } 207 208 // if still false on the message object, check for a property 209 // version also 210 if (!partialSends) { 211 partialSends = props.getBooleanProperty(MAIL_SMTP_SENDPARTIAL, false); 212 } 213 214 boolean haveGroup = false; 215 216 // enforce the requirement that all of the targets are InternetAddress 217 // instances. 218 for (int i = 0; i < addresses.length; i++) { 219 if (addresses[i] instanceof InternetAddress) { 220 // and while we're here, see if we have a groups in the address 221 // list. If we do, then 222 // we're going to need to expand these before sending. 223 if (((InternetAddress) addresses[i]).isGroup()) { 224 haveGroup = true; 225 } 226 } else { 227 throw new MessagingException("Illegal InternetAddress " + addresses[i]); 228 } 229 } 230 231 // did we find a group? Time to expand this into our full target list. 232 if (haveGroup) { 233 addresses = expandGroups(addresses); 234 } 235 236 SendStatus[] stats = new SendStatus[addresses.length]; 237 238 // create our lists for notification and exception reporting. 239 Address[] sent = null; 240 Address[] unsent = null; 241 Address[] invalid = null; 242 243 try { 244 // send sender first. If this failed, send a failure notice of the 245 // event, using the full list of 246 // addresses as the unsent, and nothing for the rest. 247 if (!connection.sendMailFrom(message)) { 248 unsent = addresses; 249 sent = new Address[0]; 250 invalid = new Address[0]; 251 // notify of the error. 252 notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); 253 254 // include the reponse information here. 255 SMTPReply last = connection.getLastServerResponse(); 256 // now send an "uber-exception" to indicate the failure. 257 throw new SMTPSendFailedException("MAIL FROM", last.getCode(), last.getMessage(), null, sent, unsent, 258 invalid); 259 } 260 261 // get the additional notification status, if available 262 String dsn = getDeliveryStatusNotification(message); 263 264 // we need to know about any failures once we've gone through the 265 // complete list, so keep a 266 // failure flag. 267 boolean sendFailure = false; 268 269 // event notifcation requires we send lists of successes and 270 // failures broken down by category. 271 // The categories are: 272 // 273 // 1) addresses successfully processed. 274 // 2) addresses deemed valid, but had a processing failure that 275 // prevented sending. 276 // 3) addressed deemed invalid (basically all other processing 277 // failures). 278 ArrayList sentAddresses = new ArrayList(); 279 ArrayList unsentAddresses = new ArrayList(); 280 ArrayList invalidAddresses = new ArrayList(); 281 282 // Now we add a MAIL TO record for each recipient. At this point, we 283 // just collect 284 for (int i = 0; i < addresses.length; i++) { 285 InternetAddress target = (InternetAddress) addresses[i]; 286 287 // write out the record now. 288 SendStatus status = connection.sendRcptTo(target, dsn); 289 stats[i] = status; 290 291 switch (status.getStatus()) { 292 // successfully sent 293 case SendStatus.SUCCESS: 294 sentAddresses.add(target); 295 break; 296 297 // we have an invalid address of some sort, or a general sending 298 // error (which we'll 299 // interpret as due to an invalid address. 300 case SendStatus.INVALID_ADDRESS: 301 case SendStatus.GENERAL_ERROR: 302 sendFailure = true; 303 invalidAddresses.add(target); 304 break; 305 306 // good address, but this was a send failure. 307 case SendStatus.SEND_FAILURE: 308 sendFailure = true; 309 unsentAddresses.add(target); 310 break; 311 } 312 } 313 314 // if we had a send failure, then we need to check if we allow 315 // partial sends. If not allowed, 316 // we abort the send operation now. 317 if (sendFailure) { 318 // if we're not allowing partial successes or we've failed on 319 // all of the addresses, it's 320 // time to abort. 321 if (!partialSends || sentAddresses.isEmpty()) { 322 // we send along the valid and invalid address lists on the 323 // notifications and 324 // exceptions. 325 // however, since we're aborting the entire send, the 326 // successes need to become 327 // members of the failure list. 328 unsentAddresses.addAll(sentAddresses); 329 330 // this one is empty. 331 sent = new Address[0]; 332 unsent = (Address[]) unsentAddresses.toArray(new Address[0]); 333 invalid = (Address[]) invalidAddresses.toArray(new Address[0]); 334 335 // go reset our connection so we can process additional 336 // sends. 337 connection.resetConnection(); 338 339 // get a list of chained exceptions for all of the failures. 340 MessagingException failures = generateExceptionChain(stats, false); 341 342 // now send an "uber-exception" to indicate the failure. 343 throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); 344 } 345 } 346 347 try { 348 // try to send the data 349 connection.sendData(message); 350 } catch (MessagingException e) { 351 // If there's an error at this point, this is a complete 352 // delivery failure. 353 // we send along the valid and invalid address lists on the 354 // notifications and 355 // exceptions. 356 // however, since we're aborting the entire send, the successes 357 // need to become 358 // members of the failure list. 359 unsentAddresses.addAll(sentAddresses); 360 361 // this one is empty. 362 sent = new Address[0]; 363 unsent = (Address[]) unsentAddresses.toArray(new Address[0]); 364 invalid = (Address[]) invalidAddresses.toArray(new Address[0]); 365 // notify of the error. 366 notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); 367 // send a send failure exception. 368 throw new SMTPSendFailedException("DATA", 0, "Send failure", e, sent, unsent, invalid); 369 } 370 371 // create our lists for notification and exception reporting from 372 // this point on. 373 sent = (Address[]) sentAddresses.toArray(new Address[0]); 374 unsent = (Address[]) unsentAddresses.toArray(new Address[0]); 375 invalid = (Address[]) invalidAddresses.toArray(new Address[0]); 376 377 // if sendFailure is true, we had an error during the address phase, 378 // but we had permission to 379 // process this as a partial send operation. Now that the data has 380 // been sent ok, it's time to 381 // report the partial failure. 382 if (sendFailure) { 383 // notify our listeners of the partial delivery. 384 notifyTransportListeners(TransportEvent.MESSAGE_PARTIALLY_DELIVERED, sent, unsent, invalid, message); 385 386 // get a list of chained exceptions for all of the failures (and 387 // the successes, if reportSuccess has been 388 // turned on). 389 MessagingException failures = generateExceptionChain(stats, reportSuccess); 390 391 // now send an "uber-exception" to indicate the failure. 392 throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); 393 } 394 395 // notify our listeners of successful delivery. 396 notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, sent, unsent, invalid, message); 397 398 // we've not had any failures, but we've been asked to report 399 // success as an exception. Do 400 // this now. 401 if (reportSuccess) { 402 // generate the chain of success exceptions (we already know 403 // there are no failure ones to report). 404 MessagingException successes = generateExceptionChain(stats, reportSuccess); 405 if (successes != null) { 406 throw successes; 407 } 408 } 409 } catch (SMTPSendFailedException e) { 410 // if this is a send failure, we've already handled 411 // notifications....just rethrow it. 412 throw e; 413 } catch (MessagingException e) { 414 // notify of the error. 415 notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); 416 throw e; 417 } 418 } 419 420 421 /** 422 * Determine what delivery status notification should 423 * be added to the RCPT TO: command. 424 * 425 * @param message The message we're sending. 426 * 427 * @return The string NOTIFY= value to add to the command. 428 */ 429 protected String getDeliveryStatusNotification(Message message) { 430 String dsn = null; 431 432 // there's an optional notification argument that can be added to 433 // MAIL TO. See if we've been 434 // provided with one. 435 436 // an SMTPMessage object is the first source 437 if (message instanceof SMTPMessage) { 438 // get the notification options 439 int options = ((SMTPMessage) message).getNotifyOptions(); 440 441 switch (options) { 442 // a zero value indicates nothing is set. 443 case 0: 444 break; 445 446 case SMTPMessage.NOTIFY_NEVER: 447 dsn = "NEVER"; 448 break; 449 450 case SMTPMessage.NOTIFY_SUCCESS: 451 dsn = "SUCCESS"; 452 break; 453 454 case SMTPMessage.NOTIFY_FAILURE: 455 dsn = "FAILURE"; 456 break; 457 458 case SMTPMessage.NOTIFY_DELAY: 459 dsn = "DELAY"; 460 break; 461 462 // now for combinations...there are few enough combinations here 463 // that we can just handle this in the switch statement rather 464 // than have to 465 // concatentate everything together. 466 case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE): 467 dsn = "SUCCESS,FAILURE"; 468 break; 469 470 case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_DELAY): 471 dsn = "SUCCESS,DELAY"; 472 break; 473 474 case (SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): 475 dsn = "FAILURE,DELAY"; 476 break; 477 478 case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): 479 dsn = "SUCCESS,FAILURE,DELAY"; 480 break; 481 } 482 } 483 484 // if still null, grab a property value (yada, yada, yada...) 485 if (dsn == null) { 486 dsn = props.getProperty(MAIL_SMTP_DSN_NOTIFY); 487 } 488 return dsn; 489 } 490 491 492 493 /** 494 * Close the connection. On completion, we'll be disconnected from the 495 * server and unable to send more data. 496 * 497 * @exception MessagingException 498 */ 499 public void close() throws MessagingException { 500 // This is done to ensure proper event notification. 501 super.close(); 502 // NB: We reuse the connection if asked to reconnect 503 connection.close(); 504 } 505 506 507 /** 508 * Turn a series of send status items into a chain of exceptions indicating 509 * the state of each send operation. 510 * 511 * @param stats 512 * The list of SendStatus items. 513 * @param reportSuccess 514 * Indicates whether we should include the report success items. 515 * 516 * @return The head of a chained list of MessagingExceptions. 517 */ 518 protected MessagingException generateExceptionChain(SendStatus[] stats, boolean reportSuccess) { 519 MessagingException current = null; 520 521 for (int i = 0; i < stats.length; i++) { 522 SendStatus status = stats[i]; 523 524 if (status != null) { 525 MessagingException nextException = stats[i].getException(reportSuccess); 526 // if there's an exception associated with this status, chain it 527 // up with the rest. 528 if (nextException != null) { 529 if (current == null) { 530 current = nextException; 531 } else { 532 current.setNextException(nextException); 533 current = nextException; 534 } 535 } 536 } 537 } 538 return current; 539 } 540 541 /** 542 * Expand the address list by converting any group addresses into single 543 * address targets. 544 * 545 * @param addresses 546 * The input array of addresses. 547 * 548 * @return The expanded array of addresses. 549 * @exception MessagingException 550 */ 551 protected Address[] expandGroups(Address[] addresses) throws MessagingException { 552 ArrayList expandedAddresses = new ArrayList(); 553 554 // run the list looking for group addresses, and add the full group list 555 // to our targets. 556 for (int i = 0; i < addresses.length; i++) { 557 InternetAddress address = (InternetAddress) addresses[i]; 558 // not a group? Just copy over to the other list. 559 if (!address.isGroup()) { 560 expandedAddresses.add(address); 561 } else { 562 // get the group address and copy each member of the group into 563 // the expanded list. 564 InternetAddress[] groupAddresses = address.getGroup(true); 565 for (int j = 1; j < groupAddresses.length; j++) { 566 expandedAddresses.add(groupAddresses[j]); 567 } 568 } 569 } 570 571 // convert back into an array. 572 return (Address[]) expandedAddresses.toArray(new Address[0]); 573 } 574 575 576 /** 577 * Retrieve the local client host name. 578 * 579 * @return The string version of the local host name. 580 * @exception SMTPTransportException 581 */ 582 public String getLocalHost() throws MessagingException { 583 return connection.getLocalHost(); 584 } 585 586 587 /** 588 * Explicitly set the local host information. 589 * 590 * @param localHost 591 * The new localHost name. 592 */ 593 public void setLocalHost(String localHost) { 594 connection.setLocalHost(localHost); 595 } 596 597 598 /** 599 * Return the current reportSuccess property. 600 * 601 * @return The current reportSuccess property. 602 */ 603 public boolean getReportSuccess() { 604 return connection.getReportSuccess(); 605 } 606 607 /** 608 * Set a new value for the reportSuccess property. 609 * 610 * @param report 611 * The new setting. 612 */ 613 public void setReportSuccess(boolean report) { 614 connection.setReportSuccess(report); 615 } 616 617 /** 618 * Return the current startTLS property. 619 * 620 * @return The current startTLS property. 621 */ 622 public boolean getStartTLS() { 623 return connection.getStartTLS(); 624 } 625 626 /** 627 * Set a new value for the startTLS property. 628 * 629 * @param start 630 * The new setting. 631 */ 632 public void setStartTLS(boolean start) { 633 connection.setStartTLS(start); 634 } 635 636 /** 637 * Retrieve the SASL realm used for DIGEST-MD5 authentication. This will 638 * either be explicitly set, or retrieved using the mail.smtp.sasl.realm 639 * session property. 640 * 641 * @return The current realm information (which can be null). 642 */ 643 public String getSASLRealm() { 644 return connection.getSASLRealm(); 645 } 646 647 /** 648 * Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton. 649 * 650 * @param name 651 * The new realm name. 652 */ 653 public void setSASLRealm(String name) { 654 connection.setSASLRealm(name); 655 } 656 }