| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.geronimo.javamail.transport.smtp; |
| |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.net.Socket; |
| import java.util.ArrayList; |
| |
| import javax.mail.Address; |
| import javax.mail.AuthenticationFailedException; |
| import javax.mail.Message; |
| import javax.mail.MessagingException; |
| import javax.mail.Session; |
| import javax.mail.Transport; |
| import javax.mail.URLName; |
| import javax.mail.event.TransportEvent; |
| import javax.mail.internet.InternetAddress; |
| import javax.mail.internet.MimeMessage; |
| import javax.mail.internet.MimeMultipart; |
| import javax.mail.internet.MimePart; |
| |
| import org.apache.geronimo.javamail.util.ProtocolProperties; |
| import org.apache.geronimo.javamail.transport.smtp.SMTPConnection.SendStatus; |
| |
| /** |
| * Simple implementation of SMTP transport. Just does plain RFC821-ish delivery. |
| * <p/> Supported properties : <p/> |
| * <ul> |
| * <li> mail.host : to set the server to deliver to. Default = localhost</li> |
| * <li> mail.smtp.port : to set the port. Default = 25</li> |
| * <li> mail.smtp.locahost : name to use for HELO/EHLO - default getHostName()</li> |
| * </ul> |
| * <p/> There is no way to indicate failure for a given recipient (it's possible |
| * to have a recipient address rejected). The sun impl throws exceptions even if |
| * others successful), but maybe we do a different way... <p/> TODO : lots. |
| * ESMTP, user/pass, indicate failure, etc... |
| * |
| * @version $Rev$ $Date$ |
| */ |
| public class SMTPTransport extends Transport { |
| /** |
| * property keys for protocol properties. The actual property name will be |
| * appended with "mail." + protocol + ".", where the protocol is either |
| * "smtp" or "smtps". |
| */ |
| protected static final String MAIL_SMTP_DSN_NOTIFY = "dsn.notify"; |
| protected static final String MAIL_SMTP_SENDPARTIAL = "sendpartial"; |
| protected static final String MAIL_SMTP_EXTENSION = "mailextension"; |
| protected static final String DEFAULT_MAIL_HOST = "localhost"; |
| |
| protected static final int DEFAULT_MAIL_SMTP_PORT = 25; |
| protected static final int DEFAULT_MAIL_SMTPS_PORT = 465; |
| |
| |
| // do we use SSL for our initial connection? |
| protected boolean sslConnection = false; |
| |
| // our accessor for protocol properties and the holder of |
| // protocol-specific information |
| protected ProtocolProperties props; |
| // our active connection object |
| protected SMTPConnection connection; |
| |
| // the last response line received from the server. |
| protected SMTPReply lastServerResponse = null; |
| |
| /** |
| * Normal constructor for an SMTPTransport() object. This constructor is |
| * used to build a transport instance for the "smtp" protocol. |
| * |
| * @param session |
| * The attached session. |
| * @param name |
| * An optional URLName object containing target information. |
| */ |
| public SMTPTransport(Session session, URLName name) { |
| this(session, name, "smtp", DEFAULT_MAIL_SMTP_PORT, false); |
| } |
| |
| |
| /** |
| * Common constructor used by the SMTPTransport and SMTPSTransport classes |
| * to do common initialization of defaults. |
| * |
| * @param session |
| * The host session instance. |
| * @param name |
| * The URLName of the target. |
| * @param protocol |
| * The protocol type (either "smtp" or "smtps". This helps us in |
| * retrieving protocol-specific session properties. |
| * @param defaultPort |
| * The default port used by this protocol. For "smtp", this will |
| * be 25. The default for "smtps" is 465. |
| * @param sslConnection |
| * Indicates whether an SSL connection should be used to initial |
| * contact the server. This is different from the STARTTLS |
| * support, which switches the connection to SSL after the |
| * initial startup. |
| */ |
| protected SMTPTransport(Session session, URLName name, String protocol, int defaultPort, boolean sslConnection) { |
| super(session, name); |
| |
| // create the protocol property holder. This gives an abstraction over the different |
| // flavors of the protocol. |
| props = new ProtocolProperties(session, protocol, sslConnection, defaultPort); |
| // the connection manages connection for the transport |
| connection = new SMTPConnection(props); |
| } |
| |
| |
| /** |
| * Connect to a server using an already created socket. This connection is |
| * just like any other connection, except we will not create a new socket. |
| * |
| * @param socket |
| * The socket connection to use. |
| */ |
| public void connect(Socket socket) throws MessagingException { |
| connection.connect(socket); |
| super.connect(); |
| } |
| |
| |
| /** |
| * Do the protocol connection for an SMTP transport. This handles server |
| * authentication, if possible. Returns false if unable to connect to the |
| * server. |
| * |
| * @param host |
| * The target host name. |
| * @param port |
| * The server port number. |
| * @param user |
| * The authentication user (if any). |
| * @param password |
| * The server password. Might not be sent directly if more |
| * sophisticated authentication is used. |
| * |
| * @return true if we were able to connect to the server properly, false for |
| * any failures. |
| * @exception MessagingException |
| */ |
| protected boolean protocolConnect(String host, int port, String username, String password) |
| throws MessagingException { |
| // the connection pool handles all of the details here. |
| return connection.protocolConnect(host, port, username, password); |
| } |
| |
| /** |
| * Send a message to multiple addressees. |
| * |
| * @param message |
| * The message we're sending. |
| * @param addresses |
| * An array of addresses to send to. |
| * |
| * @exception MessagingException |
| */ |
| public void sendMessage(Message message, Address[] addresses) throws MessagingException { |
| if (!isConnected()) { |
| throw new IllegalStateException("Not connected"); |
| } |
| // don't bother me w/ null messages or no addreses |
| if (message == null) { |
| throw new MessagingException("Null message"); |
| } |
| |
| // SMTP only handles instances of MimeMessage, not the more general |
| // message case. |
| if (!(message instanceof MimeMessage)) { |
| throw new MessagingException("SMTP can only send MimeMessages"); |
| } |
| |
| // we must have a message list. |
| if (addresses == null || addresses.length == 0) { |
| throw new MessagingException("Null or empty address array"); |
| } |
| |
| boolean reportSuccess = getReportSuccess(); |
| |
| // now see how we're configured for this send operation. |
| boolean partialSends = false; |
| |
| // this can be attached directly to the message. |
| if (message instanceof SMTPMessage) { |
| partialSends = ((SMTPMessage) message).getSendPartial(); |
| } |
| |
| // if still false on the message object, check for a property |
| // version also |
| if (!partialSends) { |
| partialSends = props.getBooleanProperty(MAIL_SMTP_SENDPARTIAL, false); |
| } |
| |
| boolean haveGroup = false; |
| |
| // enforce the requirement that all of the targets are InternetAddress |
| // instances. |
| for (int i = 0; i < addresses.length; i++) { |
| if (addresses[i] instanceof InternetAddress) { |
| // and while we're here, see if we have a groups in the address |
| // list. If we do, then |
| // we're going to need to expand these before sending. |
| if (((InternetAddress) addresses[i]).isGroup()) { |
| haveGroup = true; |
| } |
| } else { |
| throw new MessagingException("Illegal InternetAddress " + addresses[i]); |
| } |
| } |
| |
| // did we find a group? Time to expand this into our full target list. |
| if (haveGroup) { |
| addresses = expandGroups(addresses); |
| } |
| |
| SendStatus[] stats = new SendStatus[addresses.length]; |
| |
| // create our lists for notification and exception reporting. |
| Address[] sent = null; |
| Address[] unsent = null; |
| Address[] invalid = null; |
| |
| try { |
| // send sender first. If this failed, send a failure notice of the |
| // event, using the full list of |
| // addresses as the unsent, and nothing for the rest. |
| if (!connection.sendMailFrom(message)) { |
| unsent = addresses; |
| sent = new Address[0]; |
| invalid = new Address[0]; |
| // notify of the error. |
| notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); |
| |
| // include the reponse information here. |
| SMTPReply last = connection.getLastServerResponse(); |
| // now send an "uber-exception" to indicate the failure. |
| throw new SMTPSendFailedException("MAIL FROM", last.getCode(), last.getMessage(), null, sent, unsent, |
| invalid); |
| } |
| |
| // get the additional notification status, if available |
| String dsn = getDeliveryStatusNotification(message); |
| |
| // we need to know about any failures once we've gone through the |
| // complete list, so keep a |
| // failure flag. |
| boolean sendFailure = false; |
| |
| // event notifcation requires we send lists of successes and |
| // failures broken down by category. |
| // The categories are: |
| // |
| // 1) addresses successfully processed. |
| // 2) addresses deemed valid, but had a processing failure that |
| // prevented sending. |
| // 3) addressed deemed invalid (basically all other processing |
| // failures). |
| ArrayList sentAddresses = new ArrayList(); |
| ArrayList unsentAddresses = new ArrayList(); |
| ArrayList invalidAddresses = new ArrayList(); |
| |
| // Now we add a MAIL TO record for each recipient. At this point, we |
| // just collect |
| for (int i = 0; i < addresses.length; i++) { |
| InternetAddress target = (InternetAddress) addresses[i]; |
| |
| // write out the record now. |
| SendStatus status = connection.sendRcptTo(target, dsn); |
| stats[i] = status; |
| |
| switch (status.getStatus()) { |
| // successfully sent |
| case SendStatus.SUCCESS: |
| sentAddresses.add(target); |
| break; |
| |
| // we have an invalid address of some sort, or a general sending |
| // error (which we'll |
| // interpret as due to an invalid address. |
| case SendStatus.INVALID_ADDRESS: |
| case SendStatus.GENERAL_ERROR: |
| sendFailure = true; |
| invalidAddresses.add(target); |
| break; |
| |
| // good address, but this was a send failure. |
| case SendStatus.SEND_FAILURE: |
| sendFailure = true; |
| unsentAddresses.add(target); |
| break; |
| } |
| } |
| |
| // if we had a send failure, then we need to check if we allow |
| // partial sends. If not allowed, |
| // we abort the send operation now. |
| if (sendFailure) { |
| // if we're not allowing partial successes or we've failed on |
| // all of the addresses, it's |
| // time to abort. |
| if (!partialSends || sentAddresses.isEmpty()) { |
| // we send along the valid and invalid address lists on the |
| // notifications and |
| // exceptions. |
| // however, since we're aborting the entire send, the |
| // successes need to become |
| // members of the failure list. |
| unsentAddresses.addAll(sentAddresses); |
| |
| // this one is empty. |
| sent = new Address[0]; |
| unsent = (Address[]) unsentAddresses.toArray(new Address[0]); |
| invalid = (Address[]) invalidAddresses.toArray(new Address[0]); |
| |
| // go reset our connection so we can process additional |
| // sends. |
| connection.resetConnection(); |
| |
| // get a list of chained exceptions for all of the failures. |
| MessagingException failures = generateExceptionChain(stats, false); |
| |
| // now send an "uber-exception" to indicate the failure. |
| throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); |
| } |
| } |
| |
| try { |
| // try to send the data |
| connection.sendData(message); |
| } catch (MessagingException e) { |
| // If there's an error at this point, this is a complete |
| // delivery failure. |
| // we send along the valid and invalid address lists on the |
| // notifications and |
| // exceptions. |
| // however, since we're aborting the entire send, the successes |
| // need to become |
| // members of the failure list. |
| unsentAddresses.addAll(sentAddresses); |
| |
| // this one is empty. |
| sent = new Address[0]; |
| unsent = (Address[]) unsentAddresses.toArray(new Address[0]); |
| invalid = (Address[]) invalidAddresses.toArray(new Address[0]); |
| // notify of the error. |
| notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); |
| // send a send failure exception. |
| throw new SMTPSendFailedException("DATA", 0, "Send failure", e, sent, unsent, invalid); |
| } |
| |
| // create our lists for notification and exception reporting from |
| // this point on. |
| sent = (Address[]) sentAddresses.toArray(new Address[0]); |
| unsent = (Address[]) unsentAddresses.toArray(new Address[0]); |
| invalid = (Address[]) invalidAddresses.toArray(new Address[0]); |
| |
| // if sendFailure is true, we had an error during the address phase, |
| // but we had permission to |
| // process this as a partial send operation. Now that the data has |
| // been sent ok, it's time to |
| // report the partial failure. |
| if (sendFailure) { |
| // notify our listeners of the partial delivery. |
| notifyTransportListeners(TransportEvent.MESSAGE_PARTIALLY_DELIVERED, sent, unsent, invalid, message); |
| |
| // get a list of chained exceptions for all of the failures (and |
| // the successes, if reportSuccess has been |
| // turned on). |
| MessagingException failures = generateExceptionChain(stats, reportSuccess); |
| |
| // now send an "uber-exception" to indicate the failure. |
| throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); |
| } |
| |
| // notify our listeners of successful delivery. |
| notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, sent, unsent, invalid, message); |
| |
| // we've not had any failures, but we've been asked to report |
| // success as an exception. Do |
| // this now. |
| if (reportSuccess) { |
| // generate the chain of success exceptions (we already know |
| // there are no failure ones to report). |
| MessagingException successes = generateExceptionChain(stats, reportSuccess); |
| if (successes != null) { |
| throw successes; |
| } |
| } |
| } catch (SMTPSendFailedException e) { |
| // if this is a send failure, we've already handled |
| // notifications....just rethrow it. |
| throw e; |
| } catch (MessagingException e) { |
| // notify of the error. |
| notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); |
| throw e; |
| } |
| } |
| |
| |
| /** |
| * Determine what delivery status notification should |
| * be added to the RCPT TO: command. |
| * |
| * @param message The message we're sending. |
| * |
| * @return The string NOTIFY= value to add to the command. |
| */ |
| protected String getDeliveryStatusNotification(Message message) { |
| String dsn = null; |
| |
| // there's an optional notification argument that can be added to |
| // MAIL TO. See if we've been |
| // provided with one. |
| |
| // an SMTPMessage object is the first source |
| if (message instanceof SMTPMessage) { |
| // get the notification options |
| int options = ((SMTPMessage) message).getNotifyOptions(); |
| |
| switch (options) { |
| // a zero value indicates nothing is set. |
| case 0: |
| break; |
| |
| case SMTPMessage.NOTIFY_NEVER: |
| dsn = "NEVER"; |
| break; |
| |
| case SMTPMessage.NOTIFY_SUCCESS: |
| dsn = "SUCCESS"; |
| break; |
| |
| case SMTPMessage.NOTIFY_FAILURE: |
| dsn = "FAILURE"; |
| break; |
| |
| case SMTPMessage.NOTIFY_DELAY: |
| dsn = "DELAY"; |
| break; |
| |
| // now for combinations...there are few enough combinations here |
| // that we can just handle this in the switch statement rather |
| // than have to |
| // concatentate everything together. |
| case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE): |
| dsn = "SUCCESS,FAILURE"; |
| break; |
| |
| case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_DELAY): |
| dsn = "SUCCESS,DELAY"; |
| break; |
| |
| case (SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): |
| dsn = "FAILURE,DELAY"; |
| break; |
| |
| case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): |
| dsn = "SUCCESS,FAILURE,DELAY"; |
| break; |
| } |
| } |
| |
| // if still null, grab a property value (yada, yada, yada...) |
| if (dsn == null) { |
| dsn = props.getProperty(MAIL_SMTP_DSN_NOTIFY); |
| } |
| return dsn; |
| } |
| |
| |
| |
| /** |
| * Close the connection. On completion, we'll be disconnected from the |
| * server and unable to send more data. |
| * |
| * @exception MessagingException |
| */ |
| public void close() throws MessagingException { |
| // This is done to ensure proper event notification. |
| super.close(); |
| // NB: We reuse the connection if asked to reconnect |
| connection.close(); |
| } |
| |
| |
| /** |
| * Turn a series of send status items into a chain of exceptions indicating |
| * the state of each send operation. |
| * |
| * @param stats |
| * The list of SendStatus items. |
| * @param reportSuccess |
| * Indicates whether we should include the report success items. |
| * |
| * @return The head of a chained list of MessagingExceptions. |
| */ |
| protected MessagingException generateExceptionChain(SendStatus[] stats, boolean reportSuccess) { |
| MessagingException current = null; |
| |
| for (int i = 0; i < stats.length; i++) { |
| SendStatus status = stats[i]; |
| |
| if (status != null) { |
| MessagingException nextException = stats[i].getException(reportSuccess); |
| // if there's an exception associated with this status, chain it |
| // up with the rest. |
| if (nextException != null) { |
| if (current == null) { |
| current = nextException; |
| } else { |
| current.setNextException(nextException); |
| current = nextException; |
| } |
| } |
| } |
| } |
| return current; |
| } |
| |
| /** |
| * Expand the address list by converting any group addresses into single |
| * address targets. |
| * |
| * @param addresses |
| * The input array of addresses. |
| * |
| * @return The expanded array of addresses. |
| * @exception MessagingException |
| */ |
| protected Address[] expandGroups(Address[] addresses) throws MessagingException { |
| ArrayList expandedAddresses = new ArrayList(); |
| |
| // run the list looking for group addresses, and add the full group list |
| // to our targets. |
| for (int i = 0; i < addresses.length; i++) { |
| InternetAddress address = (InternetAddress) addresses[i]; |
| // not a group? Just copy over to the other list. |
| if (!address.isGroup()) { |
| expandedAddresses.add(address); |
| } else { |
| // get the group address and copy each member of the group into |
| // the expanded list. |
| InternetAddress[] groupAddresses = address.getGroup(true); |
| for (int j = 1; j < groupAddresses.length; j++) { |
| expandedAddresses.add(groupAddresses[j]); |
| } |
| } |
| } |
| |
| // convert back into an array. |
| return (Address[]) expandedAddresses.toArray(new Address[0]); |
| } |
| |
| |
| /** |
| * Retrieve the local client host name. |
| * |
| * @return The string version of the local host name. |
| * @exception SMTPTransportException |
| */ |
| public String getLocalHost() throws MessagingException { |
| return connection.getLocalHost(); |
| } |
| |
| |
| /** |
| * Explicitly set the local host information. |
| * |
| * @param localHost |
| * The new localHost name. |
| */ |
| public void setLocalHost(String localHost) { |
| connection.setLocalHost(localHost); |
| } |
| |
| |
| /** |
| * Return the current reportSuccess property. |
| * |
| * @return The current reportSuccess property. |
| */ |
| public boolean getReportSuccess() { |
| return connection.getReportSuccess(); |
| } |
| |
| /** |
| * Set a new value for the reportSuccess property. |
| * |
| * @param report |
| * The new setting. |
| */ |
| public void setReportSuccess(boolean report) { |
| connection.setReportSuccess(report); |
| } |
| |
| /** |
| * Return the current startTLS property. |
| * |
| * @return The current startTLS property. |
| */ |
| public boolean getStartTLS() { |
| return connection.getStartTLS(); |
| } |
| |
| /** |
| * Set a new value for the startTLS property. |
| * |
| * @param start |
| * The new setting. |
| */ |
| public void setStartTLS(boolean start) { |
| connection.setStartTLS(start); |
| } |
| |
| /** |
| * Retrieve the SASL realm used for DIGEST-MD5 authentication. This will |
| * either be explicitly set, or retrieved using the mail.smtp.sasl.realm |
| * session property. |
| * |
| * @return The current realm information (which can be null). |
| */ |
| public String getSASLRealm() { |
| return connection.getSASLRealm(); |
| } |
| |
| /** |
| * Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton. |
| * |
| * @param name |
| * The new realm name. |
| */ |
| public void setSASLRealm(String name) { |
| connection.setSASLRealm(name); |
| } |
| } |