Package org.apache.geronimo.javamail.transport.smtp

Source Code of org.apache.geronimo.javamail.transport.smtp.SMTPTransport$SendStatus

/**
*
* Copyright 2003-2005 The Apache Software Foundation
*
*  Licensed 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: 422788 $ $Date: 2006-07-17 14:40:22 -0400 (Mon, 17 Jul 2006) $
*/
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);
        }

        // first 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);
            }
        }

        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);
                    }
                } 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 {
            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);
    }
}
TOP

Related Classes of org.apache.geronimo.javamail.transport.smtp.SMTPTransport$SendStatus

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.