| /* |
| * 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.InputStream; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| import java.io.UnsupportedEncodingException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.net.InetAddress; |
| import java.net.Socket; |
| import java.net.SocketException; |
| import java.net.UnknownHostException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.StringTokenizer; |
| |
| 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.net.ssl.SSLSocket; |
| |
| import org.apache.geronimo.javamail.authentication.ClientAuthenticator; |
| import org.apache.geronimo.javamail.authentication.CramMD5Authenticator; |
| import org.apache.geronimo.javamail.authentication.DigestMD5Authenticator; |
| import org.apache.geronimo.javamail.authentication.LoginAuthenticator; |
| import org.apache.geronimo.javamail.authentication.PlainAuthenticator; |
| import org.apache.geronimo.javamail.util.MIMEOutputStream; |
| import org.apache.geronimo.javamail.util.TraceInputStream; |
| import org.apache.geronimo.javamail.util.TraceOutputStream; |
| import org.apache.geronimo.mail.util.Base64; |
| import org.apache.geronimo.mail.util.XText; |
| |
| /** |
| * 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 { |
| |
| /** |
| * constants for EOL termination |
| */ |
| protected static final char CR = '\r'; |
| |
| protected static final char LF = '\n'; |
| |
| /** |
| * property keys for top level session properties. |
| */ |
| protected static final String MAIL_LOCALHOST = "mail.localhost"; |
| |
| protected static final String MAIL_SSLFACTORY_CLASS = "mail.SSLSocketFactory.class"; |
| |
| /** |
| * 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_AUTH = "auth"; |
| |
| protected static final String MAIL_SMTP_PORT = "port"; |
| |
| protected static final String MAIL_SMTP_LOCALHOST = "localhost"; |
| |
| protected static final String MAIL_SMTP_TIMEOUT = "timeout"; |
| |
| protected static final String MAIL_SMTP_SASL_REALM = "sasl.realm"; |
| |
| protected static final String MAIL_SMTP_TLS = "starttls.enable"; |
| |
| protected static final String MAIL_SMTP_FACTORY_CLASS = "socketFactory.class"; |
| |
| protected static final String MAIL_SMTP_FACTORY_FALLBACK = "socketFactory.fallback"; |
| |
| protected static final String MAIL_SMTP_FACTORY_PORT = "socketFactory.port"; |
| |
| protected static final String MAIL_SMTP_REPORT_SUCCESS = "reportsuccess"; |
| |
| protected static final String MAIL_SMTP_STARTTLS_ENABLE = "starttls.enable"; |
| |
| protected static final String MAIL_SMTP_DSN_NOTIFY = "dsn.notify"; |
| |
| protected static final String MAIL_SMTP_SENDPARTIAL = "sendpartial"; |
| |
| protected static final String MAIL_SMTP_LOCALADDRESS = "localaddress"; |
| |
| protected static final String MAIL_SMTP_LOCALPORT = "localport"; |
| |
| protected static final String MAIL_SMTP_QUITWAIT = "quitwait"; |
| |
| protected static final String MAIL_SMTP_FROM = "from"; |
| |
| protected static final String MAIL_SMTP_DSN_RET = "dsn.ret"; |
| |
| protected static final String MAIL_SMTP_SUBMITTER = "submitter"; |
| |
| protected static final String MAIL_SMTP_EXTENSION = "mailextension"; |
| |
| protected static final String MAIL_SMTP_EHLO = "ehlo"; |
| |
| protected static final String MAIL_SMTP_ENCODE_TRACE = "encodetrace"; |
| |
| protected static final int MIN_MILLIS = 1000 * 60; |
| |
| protected static final int TIMEOUT = MIN_MILLIS * 5; |
| |
| 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; |
| |
| // SMTP reply codes |
| protected static final int SERVICE_READY = 220; |
| |
| protected static final int SERVICE_CLOSING = 221; |
| |
| protected static final int AUTHENTICATION_COMPLETE = 235; |
| |
| protected static final int COMMAND_ACCEPTED = 250; |
| |
| protected static final int ADDRESS_NOT_LOCAL = 251; |
| |
| protected static final int AUTHENTICATION_CHALLENGE = 334; |
| |
| protected static final int START_MAIL_INPUT = 354; |
| |
| protected static final int SERVICE_NOT_AVAILABLE = 421; |
| |
| protected static final int MAILBOX_BUSY = 450; |
| |
| protected static final int PROCESSING_ERROR = 451; |
| |
| protected static final int INSUFFICIENT_STORAGE = 452; |
| |
| protected static final int COMMAND_SYNTAX_ERROR = 500; |
| |
| protected static final int PARAMETER_SYNTAX_ERROR = 501; |
| |
| protected static final int COMMAND_NOT_IMPLEMENTED = 502; |
| |
| protected static final int INVALID_COMMAND_SEQUENCE = 503; |
| |
| protected static final int COMMAND_PARAMETER_NOT_IMPLEMENTED = 504; |
| |
| protected static final int MAILBOX_NOT_FOUND = 550; |
| |
| protected static final int USER_NOT_LOCAL = 551; |
| |
| protected static final int MAILBOX_FULL = 552; |
| |
| protected static final int INVALID_MAILBOX = 553; |
| |
| protected static final int TRANSACTION_FAILED = 553; |
| |
| protected static final String AUTHENTICATION_PLAIN = "PLAIN"; |
| |
| protected static final String AUTHENTICATION_LOGIN = "LOGIN"; |
| |
| protected static final String AUTHENTICATION_CRAMMD5 = "CRAM-MD5"; |
| |
| protected static final String AUTHENTICATION_DIGESTMD5 = "DIGEST-MD5"; |
| |
| // the protocol we're working with. This will be either "smtp" or "smtps". |
| protected String protocol; |
| |
| // the target host |
| protected String host; |
| |
| // the default port to use for this protocol (differs between "smtp" and |
| // "smtps"). |
| protected int defaultPort; |
| |
| // the target server port. |
| protected int port; |
| |
| // the connection socket...can be a plain socket or SSLSocket, if TLS is |
| // being used. |
| protected Socket socket; |
| |
| // our local host name |
| protected String localHost; |
| |
| // input stream used to read data. If Sasl is in use, this might be other |
| // than the |
| // direct access to the socket input stream. |
| protected InputStream inputStream; |
| |
| // the other end of the connection pipeline. |
| protected OutputStream outputStream; |
| |
| // list of authentication mechanisms supported by the server |
| protected HashMap serverAuthenticationMechanisms; |
| |
| // map of server extension arguments |
| protected HashMap serverExtensionArgs; |
| |
| // do we report success after completion of each mail send. |
| protected boolean reportSuccess; |
| |
| // does the server support transport level security? |
| protected boolean serverTLS = false; |
| |
| // is TLS enabled on our part? |
| protected boolean useTLS = false; |
| |
| // do we use SSL for our initial connection? |
| protected boolean sslConnection = false; |
| |
| // the username we connect with |
| protected String username; |
| |
| // the authentication password. |
| protected String password; |
| |
| // the target SASL realm (normally null unless explicitly set or we have an |
| // authentication mechanism that |
| // requires it. |
| protected String realm; |
| |
| // the last response line received from the server. |
| protected SMTPReply lastServerResponse = null; |
| |
| // our session provided debug output stream. |
| protected PrintStream debugStream; |
| |
| /** |
| * 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); |
| this.protocol = protocol; |
| |
| // these are defaults based on what the superclass specifies. |
| this.defaultPort = defaultPort; |
| this.sslConnection = sslConnection; |
| // check to see if we need to throw an exception after a send operation. |
| reportSuccess = isProtocolPropertyTrue(MAIL_SMTP_REPORT_SUCCESS); |
| // and also check for TLS enablement. |
| useTLS = isProtocolPropertyTrue(MAIL_SMTP_STARTTLS_ENABLE); |
| |
| // get our debug output. |
| debugStream = session.getDebugOut(); |
| } |
| |
| /** |
| * 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 { |
| this.socket = 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 { |
| if (debug) { |
| debugOut("Connecting to server " + host + ":" + port + " for user " + username); |
| } |
| |
| // now check to see if we need to authenticate. If we need this, then |
| // we must have a username and |
| // password specified. Failing this may result in a user prompt to |
| // collect the information. |
| boolean mustAuthenticate = isProtocolPropertyTrue(MAIL_SMTP_AUTH); |
| |
| // if we need to authenticate, and we don't have both a userid and |
| // password, then we fail this |
| // immediately. The Service.connect() method will try to obtain the user |
| // information and retry the |
| // connection one time. |
| if (mustAuthenticate && (username == null || password == null)) { |
| return false; |
| } |
| |
| // if the port is defaulted, then see if we have something configured in |
| // the session. |
| // if not configured, we just use the default default. |
| if (port == -1) { |
| // take the default first. |
| port = defaultPort; |
| String configuredPort = getProtocolProperty(MAIL_SMTP_PORT); |
| if (configuredPort != null) { |
| port = Integer.parseInt(configuredPort); |
| } |
| } |
| |
| // Before we do anything, let's make sure that we succesfully received a host |
| if ( host == null ) { |
| host = DEFAULT_MAIL_HOST; |
| } |
| |
| try { |
| |
| // create socket and connect to server. |
| getConnection(host, port, username, password); |
| |
| // receive welcoming message |
| if (!getWelcome()) { |
| throw new MessagingException("Error in getting welcome msg"); |
| } |
| |
| // say hello |
| if (!sendHandshake()) { |
| throw new MessagingException("Error in saying EHLO to server"); |
| } |
| |
| // authenticate with the server, if necessary |
| if (!processAuthentication()) { |
| if (debug) { |
| debugOut("User authentication failure"); |
| } |
| throw new AuthenticationFailedException("Error authenticating with server"); |
| } |
| } catch (IOException e) { |
| if (debug) { |
| debugOut("I/O exception establishing connection", e); |
| } |
| throw new MessagingException("Connection error", e); |
| } |
| return true; |
| } |
| |
| /** |
| * 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 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 (!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 = lastServerResponse; |
| // now send an "uber-exception" to indicate the failure. |
| throw new SMTPSendFailedException("MAIL FROM", last.getCode(), last.getMessage(), null, sent, unsent, |
| invalid); |
| } |
| |
| 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 = getProtocolProperty(MAIL_SMTP_DSN_NOTIFY); |
| } |
| |
| // 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 = 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) { |
| // 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 = isProtocolPropertyTrue(MAIL_SMTP_SENDPARTIAL); |
| } |
| |
| // 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. |
| 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 |
| 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, getReportSuccess()); |
| |
| // 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; |
| } |
| } |
| |
| /** |
| * 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 { |
| // if we're already closed, get outta here. |
| if (socket == null) { |
| return; |
| } |
| try { |
| // say goodbye |
| sendQuit(); |
| } finally { |
| // and close up the connection. We do this in a finally block to |
| // make sure the connection |
| // is shut down even if quit gets an error. |
| closeServerConnection(); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Reset the server connection after an error. |
| * |
| * @exception MessagingException |
| */ |
| protected void resetConnection() throws MessagingException { |
| // we want the caller to retrieve the last response responsbile for |
| // requiring the reset, so save and |
| // restore that info around the reset. |
| SMTPReply last = lastServerResponse; |
| |
| // send a reset command. |
| SMTPReply line = sendCommand("RSET"); |
| |
| // if this did not reset ok, just close the connection |
| if (line.getCode() != COMMAND_ACCEPTED) { |
| close(); |
| } |
| // restore this. |
| lastServerResponse = last; |
| } |
| |
| /** |
| * 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]); |
| } |
| |
| /** |
| * Create a transport connection object and connect it to the target server. |
| * |
| * @param host |
| * The target server host. |
| * @param port |
| * The connection port. |
| * |
| * @exception MessagingException |
| */ |
| protected void getConnection(String host, int port, String username, String password) throws IOException { |
| this.host = host; |
| this.port = port; |
| this.username = username; |
| this.password = password; |
| // and see if STARTTLS is enabled. |
| useTLS = isProtocolPropertyTrue(MAIL_SMTP_TLS); |
| serverAuthenticationMechanisms = new HashMap(); |
| // We might have been passed a socket to connect with...if not, we need |
| // to create one of the correct type. |
| if (socket == null) { |
| // if this is the "smtps" protocol, we start with an SSLSocket |
| if (sslConnection) { |
| getConnectedSSLSocket(); |
| } else { |
| getConnectedSocket(); |
| } |
| } |
| // if we already have a socket, get some information from it and |
| // override what we've been passed. |
| else { |
| port = socket.getPort(); |
| host = socket.getInetAddress().getHostName(); |
| } |
| // now set up the input/output streams. |
| inputStream = new TraceInputStream(socket.getInputStream(), debugStream, debug, |
| isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); |
| ; |
| outputStream = new TraceOutputStream(socket.getOutputStream(), debugStream, debug, |
| isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); |
| } |
| |
| /** |
| * Get a property associated with this mail protocol. |
| * |
| * @param name |
| * The name of the property. |
| * |
| * @return The property value (returns null if the property has not been |
| * set). |
| */ |
| protected String getProtocolProperty(String name) { |
| // the name we're given is the least qualified part of the name. We |
| // construct the full property name |
| // using the protocol (either "smtp" or "smtps"). |
| String fullName = "mail." + protocol + "." + name; |
| return getSessionProperty(fullName); |
| } |
| |
| /** |
| * Get a property associated with this mail session. |
| * |
| * @param name |
| * The name of the property. |
| * |
| * @return The property value (returns null if the property has not been |
| * set). |
| */ |
| protected String getSessionProperty(String name) { |
| return session.getProperty(name); |
| } |
| |
| /** |
| * Get a property associated with this mail session. Returns the provided |
| * default if it doesn't exist. |
| * |
| * @param name |
| * The name of the property. |
| * @param defaultValue |
| * The default value to return if the property doesn't exist. |
| * |
| * @return The property value (returns defaultValue if the property has not |
| * been set). |
| */ |
| protected String getSessionProperty(String name, String defaultValue) { |
| String result = session.getProperty(name); |
| if (result == null) { |
| return defaultValue; |
| } |
| return result; |
| } |
| |
| /** |
| * Get a property associated with this mail session. Returns the provided |
| * default if it doesn't exist. |
| * |
| * @param name |
| * The name of the property. |
| * @param defaultValue |
| * The default value to return if the property doesn't exist. |
| * |
| * @return The property value (returns defaultValue if the property has not |
| * been set). |
| */ |
| protected String getProtocolProperty(String name, String defaultValue) { |
| // the name we're given is the least qualified part of the name. We |
| // construct the full property name |
| // using the protocol (either "smtp" or "smtps"). |
| String fullName = "mail." + protocol + "." + name; |
| return getSessionProperty(fullName, defaultValue); |
| } |
| |
| /** |
| * Get a property associated with this mail session as an integer value. |
| * Returns the default value if the property doesn't exist or it doesn't |
| * have a valid int value. |
| * |
| * @param name |
| * The name of the property. |
| * @param defaultValue |
| * The default value to return if the property doesn't exist. |
| * |
| * @return The property value converted to an int. |
| */ |
| protected int getIntSessionProperty(String name, int defaultValue) { |
| String result = getSessionProperty(name); |
| if (result != null) { |
| try { |
| // convert into an int value. |
| return Integer.parseInt(result); |
| } catch (NumberFormatException e) { |
| } |
| } |
| // return default value if it doesn't exist is isn't convertable. |
| return defaultValue; |
| } |
| |
| /** |
| * Get a property associated with this mail session as an integer value. |
| * Returns the default value if the property doesn't exist or it doesn't |
| * have a valid int value. |
| * |
| * @param name |
| * The name of the property. |
| * @param defaultValue |
| * The default value to return if the property doesn't exist. |
| * |
| * @return The property value converted to an int. |
| */ |
| protected int getIntProtocolProperty(String name, int defaultValue) { |
| // the name we're given is the least qualified part of the name. We |
| // construct the full property name |
| // using the protocol (either "smtp" or "smtps"). |
| String fullName = "mail." + protocol + "." + name; |
| return getIntSessionProperty(fullName, defaultValue); |
| } |
| |
| /** |
| * Process a session property as a boolean value, returning either true or |
| * false. |
| * |
| * @return True if the property value is "true". Returns false for any other |
| * value (including null). |
| */ |
| protected boolean isProtocolPropertyTrue(String name) { |
| // the name we're given is the least qualified part of the name. We |
| // construct the full property name |
| // using the protocol (either "smtp" or "smtps"). |
| String fullName = "mail." + protocol + "." + name; |
| return isSessionPropertyTrue(fullName); |
| } |
| |
| /** |
| * Process a session property as a boolean value, returning either true or |
| * false. |
| * |
| * @return True if the property value is "true". Returns false for any other |
| * value (including null). |
| */ |
| protected boolean isSessionPropertyTrue(String name) { |
| String property = session.getProperty(name); |
| if (property != null) { |
| return property.equals("true"); |
| } |
| return false; |
| } |
| |
| /** |
| * Process a session property as a boolean value, returning either true or |
| * false. |
| * |
| * @return True if the property value is "false". Returns false for other |
| * value (including null). |
| */ |
| protected boolean isSessionPropertyFalse(String name) { |
| String property = session.getProperty(name); |
| if (property != null) { |
| return property.equals("false"); |
| } |
| return false; |
| } |
| |
| /** |
| * Process a session property as a boolean value, returning either true or |
| * false. |
| * |
| * @return True if the property value is "false". Returns false for other |
| * value (including null). |
| */ |
| protected boolean isProtocolPropertyFalse(String name) { |
| // the name we're given is the least qualified part of the name. We |
| // construct the full property name |
| // using the protocol (either "smtp" or "smtps"). |
| String fullName = "mail." + protocol + "." + name; |
| return isSessionPropertyTrue(fullName); |
| } |
| |
| /** |
| * Close the server connection at termination. |
| */ |
| protected void closeServerConnection() { |
| try { |
| socket.close(); |
| } catch (IOException ignored) { |
| } |
| |
| socket = null; |
| inputStream = null; |
| outputStream = null; |
| } |
| |
| /** |
| * Creates a connected socket |
| * |
| * @exception MessagingException |
| */ |
| protected void getConnectedSocket() throws IOException { |
| if (debug) { |
| debugOut("Attempting plain socket connection to server " + host + ":" + port); |
| } |
| |
| // the socket factory can be specified via a session property. By |
| // default, we just directly |
| // instantiate a socket without using a factor. |
| String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS); |
| |
| // there are several protocol properties that can be set to tune the |
| // created socket. We need to |
| // retrieve those bits before creating the socket. |
| int timeout = getIntProtocolProperty(MAIL_SMTP_TIMEOUT, -1); |
| InetAddress localAddress = null; |
| // see if we have a local address override. |
| String localAddrProp = getProtocolProperty(MAIL_SMTP_LOCALADDRESS); |
| if (localAddrProp != null) { |
| localAddress = InetAddress.getByName(localAddrProp); |
| } |
| |
| // check for a local port...default is to allow socket to choose. |
| int localPort = getIntProtocolProperty(MAIL_SMTP_LOCALPORT, 0); |
| |
| socket = null; |
| |
| // if there is no socket factory defined (normal), we just create a |
| // socket directly. |
| if (socketFactory == null) { |
| socket = new Socket(host, port, localAddress, localPort); |
| } |
| |
| else { |
| try { |
| int socketFactoryPort = getIntProtocolProperty(MAIL_SMTP_FACTORY_PORT, -1); |
| |
| // we choose the port used by the socket based on overrides. |
| Integer portArg = new Integer(socketFactoryPort == -1 ? port : socketFactoryPort); |
| |
| // use the current context loader to resolve this. |
| ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| Class factoryClass = loader.loadClass(socketFactory); |
| |
| // done indirectly, we need to invoke the method using |
| // reflection. |
| // This retrieves a factory instance. |
| Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); |
| Object defFactory = getDefault.invoke(new Object(), new Object[0]); |
| |
| // now that we have the factory, there are two different |
| // createSocket() calls we use, |
| // depending on whether we have a localAddress override. |
| |
| if (localAddress != null) { |
| // retrieve the createSocket(String, int, InetAddress, int) |
| // method. |
| Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE }; |
| Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); |
| |
| Object[] createSocketArgs = new Object[] { host, portArg, localAddress, new Integer(localPort) }; |
| socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); |
| } else { |
| // retrieve the createSocket(String, int) method. |
| Class[] createSocketSig = new Class[] { String.class, Integer.TYPE }; |
| Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); |
| |
| Object[] createSocketArgs = new Object[] { host, portArg }; |
| socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); |
| } |
| } catch (Throwable e) { |
| // if a socket factor is specified, then we may need to fall |
| // back to a default. This behavior |
| // is controlled by (surprise) more session properties. |
| if (isProtocolPropertyTrue(MAIL_SMTP_FACTORY_FALLBACK)) { |
| if (debug) { |
| debugOut("First plain socket attempt faile, falling back to default factory", e); |
| } |
| socket = new Socket(host, port, localAddress, localPort); |
| } |
| // we have an exception. We're going to throw an IOException, |
| // which may require unwrapping |
| // or rewrapping the exception. |
| else { |
| // we have an exception from the reflection, so unwrap the |
| // base exception |
| if (e instanceof InvocationTargetException) { |
| e = ((InvocationTargetException) e).getTargetException(); |
| } |
| |
| if (debug) { |
| debugOut("Plain socket creation failure", e); |
| } |
| |
| // throw this as an IOException, with the original exception |
| // attached. |
| IOException ioe = new IOException("Error connecting to " + host + ", " + port); |
| ioe.initCause(e); |
| throw ioe; |
| } |
| } |
| } |
| |
| if (timeout >= 0) { |
| socket.setSoTimeout(timeout); |
| } |
| } |
| |
| /** |
| * Creates a connected SSL socket for an initial SSL connection. |
| * |
| * @exception MessagingException |
| */ |
| protected void getConnectedSSLSocket() throws IOException { |
| if (debug) { |
| debugOut("Attempting SSL socket connection to server " + host + ":" + port); |
| } |
| // the socket factory can be specified via a protocol property, a |
| // session property, and if all else |
| // fails (which it usually does), we fall back to the standard factory |
| // class. |
| String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS, getSessionProperty(MAIL_SSLFACTORY_CLASS, |
| "javax.net.ssl.SSLSocketFactory")); |
| |
| // there are several protocol properties that can be set to tune the |
| // created socket. We need to |
| // retrieve those bits before creating the socket. |
| int timeout = getIntProtocolProperty(MAIL_SMTP_TIMEOUT, -1); |
| InetAddress localAddress = null; |
| // see if we have a local address override. |
| String localAddrProp = getProtocolProperty(MAIL_SMTP_LOCALADDRESS); |
| if (localAddrProp != null) { |
| localAddress = InetAddress.getByName(localAddrProp); |
| } |
| |
| // check for a local port...default is to allow socket to choose. |
| int localPort = getIntProtocolProperty(MAIL_SMTP_LOCALPORT, 0); |
| |
| socket = null; |
| |
| // if there is no socket factory defined (normal), we just create a |
| // socket directly. |
| if (socketFactory == null) { |
| socket = new Socket(host, port, localAddress, localPort); |
| } |
| |
| else { |
| // we'll try this with potentially two different factories if we're |
| // allowed to fall back. |
| boolean fallback = isProtocolPropertyTrue(MAIL_SMTP_FACTORY_FALLBACK); |
| |
| while (true) { |
| try { |
| if (debug) { |
| debugOut("Creating SSL socket using factory " + socketFactory); |
| } |
| |
| int socketFactoryPort = getIntProtocolProperty(MAIL_SMTP_FACTORY_PORT, -1); |
| |
| // we choose the port used by the socket based on overrides. |
| Integer portArg = new Integer(socketFactoryPort == -1 ? port : socketFactoryPort); |
| |
| // use the current context loader to resolve this. |
| ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| Class factoryClass = loader.loadClass(socketFactory); |
| |
| // done indirectly, we need to invoke the method using |
| // reflection. |
| // This retrieves a factory instance. |
| Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); |
| Object defFactory = getDefault.invoke(new Object(), new Object[0]); |
| |
| // now that we have the factory, there are two different |
| // createSocket() calls we use, |
| // depending on whether we have a localAddress override. |
| |
| if (localAddress != null) { |
| // retrieve the createSocket(String, int, InetAddress, |
| // int) method. |
| Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, |
| Integer.TYPE }; |
| Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); |
| |
| Object[] createSocketArgs = new Object[] { host, portArg, localAddress, new Integer(localPort) }; |
| socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); |
| } else { |
| // retrieve the createSocket(String, int) method. |
| Class[] createSocketSig = new Class[] { String.class, Integer.TYPE }; |
| Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); |
| |
| Object[] createSocketArgs = new Object[] { host, portArg }; |
| socket = (Socket) createSocket.invoke(defFactory, createSocketArgs); |
| } |
| // now break out and configure the socket. |
| break; |
| } catch (Throwable e) { |
| // if we're allowed to fallback, then use the default |
| // factory and try this again. We only |
| // allow this to happen once. |
| if (fallback) { |
| if (debug) { |
| debugOut("First attempt at creating SSL socket failed, falling back to default factory"); |
| } |
| socketFactory = "javax.net.ssl.SSLSocketFactory"; |
| fallback = false; |
| continue; |
| } |
| // we have an exception. We're going to throw an |
| // IOException, which may require unwrapping |
| // or rewrapping the exception. |
| else { |
| // we have an exception from the reflection, so unwrap |
| // the base exception |
| if (e instanceof InvocationTargetException) { |
| e = ((InvocationTargetException) e).getTargetException(); |
| } |
| |
| if (debug) { |
| debugOut("Failure creating SSL socket", e); |
| } |
| |
| // throw this as an IOException, with the original |
| // exception attached. |
| IOException ioe = new IOException("Error connecting to " + host + ", " + port); |
| ioe.initCause(e); |
| throw ioe; |
| } |
| } |
| } |
| } |
| |
| if (timeout >= 0) { |
| socket.setSoTimeout(timeout); |
| } |
| } |
| |
| /** |
| * Switch the connection to using TLS level security, switching to an SSL |
| * socket. |
| */ |
| protected void getConnectedTLSSocket() throws MessagingException { |
| if (debug) { |
| debugOut("Attempting to negotiate STARTTLS with server " + host); |
| } |
| // tell the server of our intention to start a TLS session |
| SMTPReply line = sendCommand("STARTTLS"); |
| |
| if (line.getCode() != SERVICE_READY) { |
| if (debug) { |
| debugOut("STARTTLS command rejected by SMTP server " + host); |
| } |
| throw new MessagingException("Unable to make TLS server connection"); |
| } |
| // it worked, now switch the socket into TLS mode |
| try { |
| |
| // we use the same target and port as the current connection. |
| String host = socket.getInetAddress().getHostName(); |
| int port = socket.getPort(); |
| |
| // the socket factory can be specified via a session property. By |
| // default, we use |
| // the native SSL factory. |
| String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS, "javax.net.ssl.SSLSocketFactory"); |
| |
| // use the current context loader to resolve this. |
| ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| Class factoryClass = loader.loadClass(socketFactory); |
| |
| // done indirectly, we need to invoke the method using reflection. |
| // This retrieves a factory instance. |
| Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); |
| Object defFactory = getDefault.invoke(new Object(), new Object[0]); |
| |
| // now we need to invoke createSocket() |
| Class[] createSocketSig = new Class[] { Socket.class, String.class, Integer.TYPE, Boolean.TYPE }; |
| Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); |
| |
| Object[] createSocketArgs = new Object[] { socket, host, new Integer(port), Boolean.TRUE }; |
| |
| // and finally create the socket |
| Socket sslSocket = (Socket) createSocket.invoke(defFactory, createSocketArgs); |
| |
| // if this is an instance of SSLSocket (very common), try setting |
| // the protocol to be |
| // "TLSv1". If this is some other class because of a factory |
| // override, we'll just have to |
| // accept that things will work. |
| if (sslSocket instanceof SSLSocket) { |
| ((SSLSocket) sslSocket).setEnabledProtocols(new String[] { "TLSv1" }); |
| ((SSLSocket) sslSocket).setUseClientMode(true); |
| ((SSLSocket) sslSocket).startHandshake(); |
| } |
| |
| // and finally, as a last step, replace our input streams with the |
| // secure ones. |
| // now set up the input/output streams. |
| inputStream = new TraceInputStream(sslSocket.getInputStream(), debugStream, debug, |
| isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); |
| ; |
| outputStream = new TraceOutputStream(sslSocket.getOutputStream(), debugStream, debug, |
| isProtocolPropertyTrue(MAIL_SMTP_ENCODE_TRACE)); |
| // this is our active socket now |
| socket = sslSocket; |
| |
| } catch (Exception e) { |
| if (debug) { |
| debugOut("Failure attempting to convert connection to TLS", e); |
| } |
| throw new MessagingException("Unable to convert connection to SSL", e); |
| } |
| } |
| |
| /** |
| * Get the servers welcome blob from the wire.... |
| */ |
| protected boolean getWelcome() throws MessagingException { |
| SMTPReply line = getReply(); |
| return !line.isError(); |
| } |
| |
| /** |
| * Sends the data in the message down the socket. This presumes the server |
| * is in the right place and ready for getting the DATA message and the data |
| * right place in the sequence |
| */ |
| protected void sendData(Message msg) throws MessagingException { |
| |
| // send the DATA command |
| SMTPReply line = sendCommand("DATA"); |
| |
| if (line.isError()) { |
| throw new MessagingException("Error issuing SMTP 'DATA' command: " + line); |
| } |
| |
| // now the data... I could look at the type, but |
| try { |
| // the data content has two requirements we need to meet by |
| // filtering the |
| // output stream. Requirement 1 is to conicalize any line breaks. |
| // All line |
| // breaks will be transformed into properly formed CRLF sequences. |
| // |
| // Requirement 2 is to perform byte-stuff for any line that begins |
| // with a "." |
| // so that data is not confused with the end-of-data marker (a |
| // "\r\n.\r\n" sequence. |
| // |
| // The MIME output stream performs those two functions on behalf of |
| // the content |
| // writer. |
| OutputStream mimeOut = new MIMEOutputStream(outputStream); |
| |
| msg.writeTo(mimeOut); |
| mimeOut.flush(); |
| } catch (IOException e) { |
| throw new MessagingException(e.toString()); |
| } catch (MessagingException e) { |
| throw new MessagingException(e.toString()); |
| } |
| |
| // now to finish, we send a CRLF sequence, followed by a ".". |
| sendLine(""); |
| sendLine("."); |
| |
| // use a longer time out here to give the server time to process the |
| // data. |
| try { |
| line = new SMTPReply(receiveLine(TIMEOUT * 2)); |
| } catch (MalformedSMTPReplyException e) { |
| throw new MessagingException(e.toString()); |
| } catch (MessagingException e) { |
| throw new MessagingException(e.toString()); |
| } |
| |
| if (line.isError()) { |
| throw new MessagingException("Error issuing SMTP 'DATA' command: " + line); |
| } |
| } |
| |
| /** |
| * Sends the QUIT message and receieves the response |
| */ |
| protected void sendQuit() throws MessagingException { |
| // there's yet another property that controls whether we should wait for |
| // a |
| // reply for a QUIT command. If on, just send the command and get outta |
| // here. |
| if (isProtocolPropertyTrue(MAIL_SMTP_QUITWAIT)) { |
| sendLine("QUIT"); |
| } else { |
| // handle as a real command...we're going to ignore the response. |
| sendCommand("QUIT"); |
| } |
| } |
| |
| /** |
| * Sets a receiver address for the current message |
| * |
| * @param addr |
| * The target address. |
| * @param dsn |
| * An optional notification address appended to the MAIL command. |
| * |
| * @return The status for this particular send operation. |
| * @exception MessagingException |
| */ |
| protected SendStatus sendRcptTo(InternetAddress addr, String dsn) throws MessagingException { |
| // compose the command using the fixed up email address. Normally, this |
| // involves adding |
| // "<" and ">" around the address. |
| |
| StringBuffer command = new StringBuffer(); |
| |
| // compose the first part of the command |
| command.append("RCPT TO: "); |
| command.append(fixEmailAddress(addr.getAddress())); |
| |
| // if we have DSN information, append it to the command. |
| if (dsn != null) { |
| command.append(" NOTIFY="); |
| command.append(dsn); |
| } |
| |
| // get a string version of this command. |
| String commandString = command.toString(); |
| |
| SMTPReply line = sendCommand(commandString); |
| |
| switch (line.getCode()) { |
| // these two are both successful transmissions |
| case COMMAND_ACCEPTED: |
| case ADDRESS_NOT_LOCAL: |
| // we get out of here with the status information. |
| return new SendStatus(SendStatus.SUCCESS, addr, commandString, line); |
| |
| // these are considered invalid address errors |
| case PARAMETER_SYNTAX_ERROR: |
| case INVALID_COMMAND_SEQUENCE: |
| case MAILBOX_NOT_FOUND: |
| case INVALID_MAILBOX: |
| case USER_NOT_LOCAL: |
| // we get out of here with the status information. |
| return new SendStatus(SendStatus.INVALID_ADDRESS, addr, commandString, line); |
| |
| // the command was valid, but something went wrong in the server. |
| case SERVICE_NOT_AVAILABLE: |
| case MAILBOX_BUSY: |
| case PROCESSING_ERROR: |
| case INSUFFICIENT_STORAGE: |
| case MAILBOX_FULL: |
| // we get out of here with the status information. |
| return new SendStatus(SendStatus.SEND_FAILURE, addr, commandString, line); |
| |
| // everything else is considered really bad... |
| default: |
| // we get out of here with the status information. |
| return new SendStatus(SendStatus.GENERAL_ERROR, addr, commandString, line); |
| } |
| } |
| |
| /** |
| * Set the sender for this mail. |
| * |
| * @param message |
| * The message we're sending. |
| * |
| * @exception MessagingException |
| */ |
| protected boolean sendMailFrom(Message message) throws MessagingException { |
| |
| // need to sort the from value out from a variety of sources. |
| String from = null; |
| |
| // first potential source is from the message itself, if it's an |
| // instance of SMTPMessage. |
| if (message instanceof SMTPMessage) { |
| from = ((SMTPMessage) message).getEnvelopeFrom(); |
| } |
| |
| // if not available from the message, check the protocol property next |
| if (from == null || from.length() == 0) { |
| // the from value can be set explicitly as a property |
| from = getProtocolProperty(MAIL_SMTP_FROM); |
| } |
| |
| // if not there, see if we have something in the message header. |
| if (from == null || from.length() == 0) { |
| Address[] fromAddresses = message.getFrom(); |
| |
| // if we have some addresses in the header, then take the first one |
| // as our From: address |
| if (fromAddresses != null && fromAddresses.length > 0) { |
| from = ((InternetAddress) fromAddresses[0]).getAddress(); |
| } |
| // get what the InternetAddress class believes to be the local |
| // address. |
| else { |
| from = InternetAddress.getLocalAddress(session).getAddress(); |
| } |
| } |
| |
| if (from == null || from.length() == 0) { |
| throw new MessagingException("no FROM address"); |
| } |
| |
| StringBuffer command = new StringBuffer(); |
| |
| // start building up the command |
| command.append("MAIL FROM: "); |
| command.append(fixEmailAddress(from)); |
| |
| // does this server support Delivery Status Notification? Then we may |
| // need to add some extra to the command. |
| if (supportsExtension("DSN")) { |
| String returnNotification = null; |
| |
| // the return notification stuff might be set as value on the |
| // message object itself. |
| if (message instanceof SMTPMessage) { |
| // we need to convert the option into a string value. |
| switch (((SMTPMessage) message).getReturnOption()) { |
| case SMTPMessage.RETURN_FULL: |
| returnNotification = "FULL"; |
| break; |
| |
| case SMTPMessage.RETURN_HDRS: |
| returnNotification = "HDRS"; |
| break; |
| } |
| } |
| |
| // if not obtained from the message object, it can also be set as a |
| // property. |
| if (returnNotification == null) { |
| // the DSN value is set by yet another property. |
| returnNotification = getProtocolProperty(MAIL_SMTP_DSN_RET); |
| } |
| |
| // if we have a target, add the notification stuff to our FROM |
| // command. |
| if (returnNotification != null) { |
| command.append(" RET="); |
| command.append(returnNotification); |
| } |
| } |
| |
| // if this server supports AUTH and we have submitter information, then |
| // we also add the |
| // "AUTH=" keyword to the MAIL FROM command (see RFC 2554). |
| |
| if (supportsExtension("AUTH")) { |
| String submitter = null; |
| |
| // another option that can be specified on the message object. |
| if (message instanceof SMTPMessage) { |
| submitter = ((SMTPMessage) message).getSubmitter(); |
| } |
| // if not part of the object, try for a propery version. |
| if (submitter == null) { |
| // we only send the extra keyword is a submitter is specified. |
| submitter = getProtocolProperty(MAIL_SMTP_SUBMITTER); |
| } |
| // we have one...add the keyword, plus the submitter info in xtext |
| // format (defined by RFC 1891). |
| if (submitter != null) { |
| command.append(" AUTH="); |
| try { |
| // add this encoded |
| command.append(new String(XText.encode(submitter.getBytes("US-ASCII")))); |
| } catch (UnsupportedEncodingException e) { |
| throw new MessagingException("Invalid submitter value " + submitter); |
| } |
| } |
| } |
| |
| String extension = null; |
| |
| // now see if we need to add any additional extension info to this |
| // command. The extension is not |
| // checked for validity. That's the reponsibility of the caller. |
| if (message instanceof SMTPMessage) { |
| extension = ((SMTPMessage) message).getMailExtension(); |
| } |
| // this can come either from the object or from a set property. |
| if (extension == null) { |
| extension = getProtocolProperty(MAIL_SMTP_EXTENSION); |
| } |
| |
| // have something real to add? |
| if (extension != null && extension.length() != 0) { |
| // tack this on the end with a blank delimiter. |
| command.append(' '); |
| command.append(extension); |
| } |
| |
| // and finally send the command |
| SMTPReply line = sendCommand(command.toString()); |
| |
| // 250 response indicates success. |
| return line.getCode() == COMMAND_ACCEPTED; |
| } |
| |
| /** |
| * Send a command to the server, returning the first response line back as a |
| * reply. |
| * |
| * @param data |
| * The data to send. |
| * |
| * @return A reply object with the reply line. |
| * @exception MessagingException |
| */ |
| protected SMTPReply sendCommand(String data) throws MessagingException { |
| sendLine(data); |
| return getReply(); |
| } |
| |
| /** |
| * Sends a message down the socket and terminates with the appropriate CRLF |
| */ |
| protected void sendLine(String data) throws MessagingException { |
| if (socket == null || !socket.isConnected()) { |
| throw new MessagingException("no connection"); |
| } |
| try { |
| System.out.println(">>>>>Sending data " + data + "<<<<<<"); |
| outputStream.write(data.getBytes()); |
| outputStream.write(CR); |
| outputStream.write(LF); |
| outputStream.flush(); |
| } catch (IOException e) { |
| throw new MessagingException(e.toString()); |
| } |
| } |
| |
| /** |
| * Receives one line from the server. A line is a sequence of bytes |
| * terminated by a CRLF |
| * |
| * @return the line from the server as String |
| */ |
| protected String receiveLine() throws MessagingException { |
| return receiveLine(TIMEOUT); |
| } |
| |
| /** |
| * Get a reply line for an SMTP command. |
| * |
| * @return An SMTP reply object from the stream. |
| */ |
| protected SMTPReply getReply() throws MessagingException { |
| try { |
| lastServerResponse = new SMTPReply(receiveLine()); |
| } catch (MalformedSMTPReplyException e) { |
| throw new MessagingException(e.toString()); |
| } catch (MessagingException e) { |
| throw e; |
| } |
| return lastServerResponse; |
| } |
| |
| /** |
| * Retrieve the last response received from the SMTP server. |
| * |
| * @return The raw response string (including the error code) returned from |
| * the SMTP server. |
| */ |
| public String getLastServerResponse() { |
| if (lastServerResponse == null) { |
| return ""; |
| } |
| return lastServerResponse.getReply(); |
| } |
| |
| /** |
| * Receives one line from the server. A line is a sequence of bytes |
| * terminated by a CRLF |
| * |
| * @return the line from the server as String |
| */ |
| protected String receiveLine(int delayMillis) throws MessagingException { |
| if (socket == null || !socket.isConnected()) { |
| throw new MessagingException("no connection"); |
| } |
| |
| int timeout = 0; |
| |
| try { |
| // for now, read byte for byte, looking for a CRLF |
| timeout = socket.getSoTimeout(); |
| |
| socket.setSoTimeout(delayMillis); |
| |
| StringBuffer buff = new StringBuffer(); |
| |
| int c; |
| boolean crFound = false, lfFound = false; |
| |
| while ((c = inputStream.read()) != -1 && crFound == false && lfFound == false) { |
| // we're looking for a CRLF sequence, so mark each one as seen. |
| // Any other |
| // character gets appended to the end of the buffer. |
| if (c == CR) { |
| crFound = true; |
| } else if (c == LF) { |
| lfFound = true; |
| } else { |
| buff.append((char) c); |
| } |
| } |
| |
| String line = buff.toString(); |
| return line; |
| |
| } catch (SocketException e) { |
| throw new MessagingException(e.toString()); |
| } catch (IOException e) { |
| throw new MessagingException(e.toString()); |
| } finally { |
| try { |
| socket.setSoTimeout(timeout); |
| } catch (SocketException e) { |
| // ignore - was just trying to do the decent thing... |
| } |
| } |
| } |
| |
| /** |
| * Convert an InternetAddress into a form sendable on an SMTP mail command. |
| * InternetAddress.getAddress() generally returns just the address portion |
| * of the full address, minus route address markers. We need to ensure we |
| * have an address with '<' and '>' delimiters. |
| * |
| * @param mail |
| * The mail address returned from InternetAddress.getAddress(). |
| * |
| * @return A string formatted for sending. |
| */ |
| protected String fixEmailAddress(String mail) { |
| if (mail.charAt(0) == '<') { |
| return mail; |
| } |
| return "<" + mail + ">"; |
| } |
| |
| /** |
| * Start the handshake process with the server, including setting up and |
| * TLS-level work. At the completion of this task, we should be ready to |
| * authenticate with the server, if needed. |
| */ |
| protected boolean sendHandshake() throws MessagingException { |
| // check to see what sort of initial handshake we need to make. |
| boolean useEhlo = !isProtocolPropertyFalse(MAIL_SMTP_EHLO); |
| |
| // if we're to use Ehlo, send it and then fall back to just a HELO |
| // message if it fails. |
| if (useEhlo) { |
| if (!sendEhlo()) { |
| sendHelo(); |
| } |
| } else { |
| // send the initial hello response. |
| sendHelo(); |
| } |
| |
| if (useTLS) { |
| // if we've been told to use TLS, and this server doesn't support |
| // it, then this is a failure |
| if (!serverTLS) { |
| throw new MessagingException("Server doesn't support required transport level security"); |
| } |
| // if the server supports TLS, then use it for the connection. |
| // on our connection. |
| getConnectedTLSSocket(); |
| |
| // some servers (gmail is one that I know of) only send a STARTTLS |
| // extension message on the |
| // first EHLO command. Now that we have the TLS handshaking |
| // established, we need to send a |
| // second EHLO message to retrieve the AUTH records from the server. |
| serverAuthenticationMechanisms.clear(); |
| if (!sendEhlo()) { |
| throw new MessagingException("Failure sending EHLO command to SMTP server"); |
| } |
| } |
| |
| // this worked. |
| return true; |
| } |
| |
| /** |
| * Send the EHLO command to the SMTP server. |
| * |
| * @return True if the command was accepted ok, false for any errors. |
| * @exception SMTPTransportException |
| * @exception MalformedSMTPReplyException |
| * @exception MessagingException |
| */ |
| protected boolean sendEhlo() throws MessagingException { |
| sendLine("EHLO " + getLocalHost()); |
| |
| SMTPReply line = getReply(); |
| |
| // we get a 250 code back. The first line is just a greeting, and |
| // extensions are identifed on |
| // continuations. If this fails, then we'll try once more with HELO to |
| // establish bona fides. |
| if (line.getCode() != COMMAND_ACCEPTED) { |
| return false; |
| } |
| |
| // get a fresh extension mapping table. |
| serverExtensionArgs = new HashMap(); |
| |
| // process all of the continuation lines |
| while (line.isContinued()) { |
| // get the next line |
| line = getReply(); |
| if (line.getCode() != COMMAND_ACCEPTED) { |
| // all EHLO failures go back to the HELO failback step. |
| return false; |
| } |
| // go process the extention |
| processExtension(line.getMessage()); |
| } |
| return true; |
| } |
| |
| /** |
| * Send the HELO command to the SMTP server. |
| * |
| * @exception MessagingException |
| */ |
| protected void sendHelo() throws MessagingException { |
| sendLine("HELO " + getLocalHost()); |
| |
| SMTPReply line = getReply(); |
| |
| // we get a 250 code back. The first line is just a greeting, and |
| // extensions are identifed on |
| // continuations. If this fails, then we'll try once more with HELO to |
| // establish bona fides. |
| if (line.getCode() != COMMAND_ACCEPTED) { |
| throw new MessagingException("Failure sending HELO command to SMTP server"); |
| } |
| } |
| |
| /** |
| * Retrieve the local client host name. |
| * |
| * @return The string version of the local host name. |
| * @exception SMTPTransportException |
| */ |
| public String getLocalHost() throws MessagingException { |
| if (localHost == null) { |
| |
| try { |
| localHost = InetAddress.getLocalHost().getHostName(); |
| } catch (UnknownHostException e) { |
| // fine, we're misconfigured - ignore |
| } |
| |
| if (localHost == null) { |
| localHost = getProtocolProperty(MAIL_SMTP_LOCALHOST); |
| } |
| |
| if (localHost == null) { |
| localHost = getSessionProperty(MAIL_LOCALHOST); |
| } |
| |
| if (localHost == null) { |
| throw new MessagingException("Can't get local hostname. " |
| + " Please correctly configure JDK/DNS or set mail.smtp.localhost"); |
| } |
| } |
| |
| return localHost; |
| } |
| |
| /** |
| * Return the current reportSuccess property. |
| * |
| * @return The current reportSuccess property. |
| */ |
| public boolean getReportSuccess() { |
| return reportSuccess; |
| } |
| |
| /** |
| * Set a new value for the reportSuccess property. |
| * |
| * @param report |
| * The new setting. |
| */ |
| public void setReportSuccess(boolean report) { |
| reportSuccess = report; |
| } |
| |
| /** |
| * Return the current startTLS property. |
| * |
| * @return The current startTLS property. |
| */ |
| public boolean getStartTLS() { |
| return reportSuccess; |
| } |
| |
| /** |
| * Set a new value for the startTLS property. |
| * |
| * @param start |
| * The new setting. |
| */ |
| public void setStartTLS(boolean start) { |
| useTLS = 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() { |
| // if the realm is null, retrieve it using the realm session property. |
| if (realm == null) { |
| realm = getProtocolProperty(MAIL_SMTP_SASL_REALM); |
| } |
| return realm; |
| } |
| |
| /** |
| * Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton. |
| * |
| * @param name |
| * The new realm name. |
| */ |
| public void setSASLRealm(String name) { |
| realm = name; |
| } |
| |
| /** |
| * Explicitly set the local host information. |
| * |
| * @param localHost |
| * The new localHost name. |
| */ |
| public void setLocalHost(String localHost) { |
| this.localHost = localHost; |
| } |
| |
| /** |
| * Process an extension string passed back as the EHLP response. |
| * |
| * @param extension |
| * The string value of the extension (which will be of the form |
| * "NAME arguments"). |
| */ |
| protected void processExtension(String extension) { |
| String extensionName = extension.toUpperCase(); |
| String argument = ""; |
| |
| int delimiter = extension.indexOf(' '); |
| // if we have a keyword with arguments, parse them out and add to the |
| // argument map. |
| if (delimiter != -1) { |
| extensionName = extension.substring(0, delimiter).toUpperCase(); |
| argument = extension.substring(delimiter + 1); |
| } |
| |
| // add this to the map so it can be tested later. |
| serverExtensionArgs.put(extensionName, argument); |
| |
| // process a few special ones that don't require extra parsing. |
| // AUTH and AUTH=LOGIN are handled the same |
| if (extensionName.equals("AUTH")) { |
| // if we don't have an argument on AUTH, this means LOGIN. |
| if (argument == null) { |
| serverAuthenticationMechanisms.put("LOGIN", "LOGIN"); |
| } else { |
| // The security mechanisms are blank delimited tokens. |
| StringTokenizer tokenizer = new StringTokenizer(argument); |
| |
| while (tokenizer.hasMoreTokens()) { |
| String mechanism = tokenizer.nextToken().toUpperCase(); |
| serverAuthenticationMechanisms.put(mechanism, mechanism); |
| } |
| } |
| } |
| // special case for some older servers. |
| else if (extensionName.equals("AUTH=LOGIN")) { |
| serverAuthenticationMechanisms.put("LOGIN", "LOGIN"); |
| } |
| // does this support transport level security? |
| else if (extensionName.equals("STARTTLS")) { |
| // flag this for later |
| serverTLS = true; |
| } |
| } |
| |
| /** |
| * Retrieve any argument information associated with a extension reported |
| * back by the server on the EHLO command. |
| * |
| * @param name |
| * The name of the target server extension. |
| * |
| * @return Any argument passed on a server extension. Returns null if the |
| * extension did not include an argument or the extension was not |
| * supported. |
| */ |
| public String extensionParameter(String name) { |
| if (serverExtensionArgs != null) { |
| return (String) serverExtensionArgs.get(name); |
| } |
| return null; |
| } |
| |
| /** |
| * Tests whether the target server supports a named extension. |
| * |
| * @param name |
| * The target extension name. |
| * |
| * @return true if the target server reported on the EHLO command that is |
| * supports the targer server, false if the extension was not |
| * supported. |
| */ |
| public boolean supportsExtension(String name) { |
| // this only returns null if we don't have this extension |
| return extensionParameter(name) != null; |
| } |
| |
| /** |
| * Determine if the target server supports a given authentication mechanism. |
| * |
| * @param mechanism |
| * The mechanism name. |
| * |
| * @return true if the server EHLO response indicates it supports the |
| * mechanism, false otherwise. |
| */ |
| protected boolean supportsAuthentication(String mechanism) { |
| return serverAuthenticationMechanisms.get(mechanism) != null; |
| } |
| |
| /** |
| * Authenticate with the server, if necessary (or possible). |
| * |
| * @return true if we are ok to proceed, false for an authentication |
| * failures. |
| */ |
| protected boolean processAuthentication() throws MessagingException { |
| // no authentication defined? |
| if (!isProtocolPropertyTrue(MAIL_SMTP_AUTH)) { |
| return true; |
| } |
| |
| // we need to authenticate, but we don't have userid/password |
| // information...fail this |
| // immediately. |
| if (username == null || password == null) { |
| return false; |
| } |
| |
| ClientAuthenticator authenticator = null; |
| |
| // now go through the progression of mechanisms we support, from the |
| // most secure to the |
| // least secure. |
| |
| if (supportsAuthentication(AUTHENTICATION_DIGESTMD5)) { |
| authenticator = new DigestMD5Authenticator(host, username, password, getSASLRealm()); |
| } else if (supportsAuthentication(AUTHENTICATION_CRAMMD5)) { |
| authenticator = new CramMD5Authenticator(username, password); |
| } else if (supportsAuthentication(AUTHENTICATION_LOGIN)) { |
| authenticator = new LoginAuthenticator(username, password); |
| } else if (supportsAuthentication(AUTHENTICATION_PLAIN)) { |
| authenticator = new PlainAuthenticator(username, password); |
| } else { |
| // can't find a mechanism we support in common |
| return false; |
| } |
| |
| if (debug) { |
| debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName()); |
| } |
| |
| // if the authenticator has some initial data, we compose a command |
| // containing the initial data. |
| if (authenticator.hasInitialResponse()) { |
| StringBuffer command = new StringBuffer(); |
| // the auth command initiates the handshaking. |
| command.append("AUTH "); |
| // and tell the server which mechanism we're using. |
| command.append(authenticator.getMechanismName()); |
| command.append(" "); |
| // and append the response data |
| command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)))); |
| // send the command now |
| sendLine(command.toString()); |
| } |
| // we just send an auth command with the command type. |
| else { |
| StringBuffer command = new StringBuffer(); |
| // the auth command initiates the handshaking. |
| command.append("AUTH "); |
| // and tell the server which mechanism we're using. |
| command.append(authenticator.getMechanismName()); |
| // send the command now |
| sendLine(command.toString()); |
| } |
| |
| // now process the challenge sequence. We get a 235 response back when |
| // the server accepts the |
| // authentication, and a 334 indicates we have an additional challenge. |
| while (true) { |
| // get the next line, and if it is an error response, return now. |
| SMTPReply line; |
| try { |
| line = new SMTPReply(receiveLine()); |
| } catch (MalformedSMTPReplyException e) { |
| throw new MessagingException(e.toString()); |
| } catch (MessagingException e) { |
| throw e; |
| } |
| |
| // if we get a completion return, we've passed muster, so give an |
| // authentication response. |
| if (line.getCode() == AUTHENTICATION_COMPLETE) { |
| if (debug) { |
| debugOut("Successful SMTP authentication"); |
| } |
| return true; |
| } |
| // we have an additional challenge to process. |
| else if (line.getCode() == AUTHENTICATION_CHALLENGE) { |
| // Does the authenticator think it is finished? We can't answer |
| // an additional challenge, |
| // so fail this. |
| if (authenticator.isComplete()) { |
| return false; |
| } |
| |
| // we're passed back a challenge value, Base64 encoded. |
| byte[] challenge = Base64.decode(line.getMessage().getBytes()); |
| |
| // have the authenticator evaluate and send back the encoded |
| // response. |
| sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)))); |
| } |
| // completion or challenge are the only responses we know how to |
| // handle. Anything else must |
| // be a failure. |
| else { |
| if (debug) { |
| debugOut("Authentication failure " + line); |
| } |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * Simple holder class for the address/send status duple, as we can have |
| * mixed success for a set of addresses and a message |
| */ |
| public class SendStatus { |
| public final static int SUCCESS = 0; |
| |
| public final static int INVALID_ADDRESS = 1; |
| |
| public final static int SEND_FAILURE = 2; |
| |
| public final static int GENERAL_ERROR = 3; |
| |
| // the status type of the send operation. |
| int status; |
| |
| // the address associated with this status |
| InternetAddress address; |
| |
| // the command string send to the server. |
| String cmd; |
| |
| // the reply from the server. |
| SMTPReply reply; |
| |
| /** |
| * Constructor for a SendStatus item. |
| * |
| * @param s |
| * The status type. |
| * @param a |
| * The address this is the status for. |
| * @param c |
| * The command string associated with this status. |
| * @param r |
| * The reply information from the server. |
| */ |
| public SendStatus(int s, InternetAddress a, String c, SMTPReply r) { |
| this.cmd = c; |
| this.status = s; |
| this.address = a; |
| this.reply = r; |
| } |
| |
| /** |
| * Get the status information for this item. |
| * |
| * @return The current status code. |
| */ |
| public int getStatus() { |
| return this.status; |
| } |
| |
| /** |
| * Retrieve the InternetAddress object associated with this send |
| * operation. |
| * |
| * @return The associated address object. |
| */ |
| public InternetAddress getAddress() { |
| return this.address; |
| } |
| |
| /** |
| * Retrieve the reply information associated with this send operati |
| * |
| * @return The SMTPReply object received for the operation. |
| */ |
| public SMTPReply getReply() { |
| return reply; |
| } |
| |
| /** |
| * Get the command string sent for this send operation. |
| * |
| * @return The command string for the MAIL TO command sent to the |
| * server. |
| */ |
| public String getCommand() { |
| return cmd; |
| } |
| |
| /** |
| * Get an exception object associated with this send operation. There is |
| * a mechanism for reporting send success via a send operation, so this |
| * will be either a success or failure exception. |
| * |
| * @param reportSuccess |
| * Indicates if we want success operations too. |
| * |
| * @return A newly constructed exception object. |
| */ |
| public MessagingException getException(boolean reportSuccess) { |
| if (status != SUCCESS) { |
| return new SMTPAddressFailedException(address, cmd, reply.getCode(), reply.getMessage()); |
| } else { |
| if (reportSuccess) { |
| return new SMTPAddressSucceededException(address, cmd, reply.getCode(), reply.getMessage()); |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Internal debug output routine. |
| * |
| * @param value |
| * The string value to output. |
| */ |
| protected void debugOut(String message) { |
| debugStream.println("SMTPTransport DEBUG: " + message); |
| } |
| |
| /** |
| * Internal debugging routine for reporting exceptions. |
| * |
| * @param message |
| * A message associated with the exception context. |
| * @param e |
| * The received exception. |
| */ |
| protected void debugOut(String message, Throwable e) { |
| debugOut("Received exception -> " + message); |
| debugOut("Exception message -> " + e.getMessage()); |
| e.printStackTrace(debugStream); |
| } |
| } |