You are viewing a plain text version of this content. The canonical link for it is here.
Posted to scm@geronimo.apache.org by ri...@apache.org on 2006/05/01 18:56:09 UTC

svn commit: r398634 [2/3] - in /geronimo/branches/1.1/modules/javamail-transport/src: java/org/apache/geronimo/javamail/authentication/ java/org/apache/geronimo/javamail/transport/smtp/ java/org/apache/geronimo/javamail/util/ resources/META-INF/

Modified: geronimo/branches/1.1/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java
URL: http://svn.apache.org/viewcvs/geronimo/branches/1.1/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java?rev=398634&r1=398633&r2=398634&view=diff
==============================================================================
--- geronimo/branches/1.1/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java (original)
+++ geronimo/branches/1.1/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java Mon May  1 09:56:06 2006
@@ -20,444 +20,2331 @@
 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.InetSocketAddress;
 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.URLName;
 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/>
+ * 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.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...
- *
+ * <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
      */
-    private static final char CR = 0x0D;
-    private static final char LF = 0x0A;
+    protected static final char CR = '\r';
+
+    protected static final char LF = '\n';
 
     /**
-     * property key for SMTP server to talk to
+     * property keys for top level session properties.
      */
-    private static final String MAIL_HOST = "mail.host";
-    private static final String MAIL_SMTP_LOCALHOST = "mail.smtp.localhost";
-    private static final String MAIL_SMTP_PORT = "mail.smtp.port";
+    protected static final String MAIL_LOCALHOST = "mail.localhost";
 
-    private static final int MIN_MILLIS = 1000 * 60;
-    private static final String DEFAULT_MAIL_HOST = "localhost";
-    private static final int DEFAULT_MAIL_SMTP_PORT = 25;
+    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();
+
+        System.out.println("Debug value in transport = " + debug);
     }
 
-    public void sendMessage(Message message, Address[] addresses) throws MessagingException {
-        // do it and ignore the return
-        sendMessage(addresses, message);
+    /**
+     * 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();
     }
 
-    public SendStatus[] sendMessage(Address[] addresses, Message message) throws MessagingException {
+    /**
+     * 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;
 
-        SendStatus[] stat = new SendStatus[addresses.length];
+        // 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);
+            }
 
-            // create socket and connect to server.
-            Socket s = getConnectedSocket();
+            String dsn = null;
 
-            // receive welcoming message
-            if (!getWelcome(s)) {
-                throw new MessagingException("Error in getting welcome msg");
+            // 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;
+                }
             }
 
-            // say hello
-            if (!sendHelo(s)) {
-                throw new MessagingException("Error in saying HELO to server");
+            // 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();
 
-            // send sender
-            if (!sendMailFrom(s, message.getFrom())) {
-                throw new MessagingException("Error in setting the MAIL FROM");
+            // 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;
+                }
             }
 
-            // send recipients.  Only send if not null or "", and just ignore (but log) any errors
-            for (int i = 0; i < addresses.length; i++) {
-                String to = addresses[i].toString();
+            // 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();
+                }
 
-                int status = SendStatus.SUCCESS;
+                // if still false on the message object, check for a property
+                // version also
+                if (!partialSends) {
+                    partialSends = isProtocolPropertyTrue(MAIL_SMTP_SENDPARTIAL);
+                }
 
-                if (to != null && !"".equals(to)) {
-                    if (!sendRcptTo(s, to)) {
-                        // this means it didn't like our recipient.  I say we keep going
-                        if (this.session.getDebug()) {
-                            this.session.getDebugOut().println("ERROR setting recipient " + to);
-                        }
+                // 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();
 
-                        status = SendStatus.FAIL;
-                    }
-                } else {
-                    status = SendStatus.FAIL;
+                    // 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);
                 }
+            }
 
-                stat[i] = new SendStatus(status, to);
+            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);
             }
 
-            // send data
-            if (!sendData(s, message)) {
-                throw new MessagingException("Error sending data");
+            // 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);
             }
 
-            // say goodbye
-            sendQuit(s);
+            // notify our listeners of successful delivery.
+            notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, sent, unsent, invalid, message);
 
-            try {
-                s.close();
-            } catch (IOException ignored) {
+            // 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 (SMTPTransportException e) {
-            throw new MessagingException("error", e);
-        } catch (MalformedSMTPReplyException e) {
-            throw new MessagingException("error", e);
+        } 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;
         }
-
-        return stat;
     }
 
     /**
-     * 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
+     * Close the connection. On completion, we'll be disconnected from the
+     * server and unable to send more data.
+     * 
+     * @exception MessagingException
      */
-    protected boolean sendData(Socket s, Message msg) throws SMTPTransportException, MalformedSMTPReplyException {
-        if (msg == null) {
-            throw new SMTPTransportException("invalid message");
+    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();
         }
+    }
 
-        // send the DATA command
-        sendLine(s, "DATA");
+    /**
+     * 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;
 
-        SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
+        for (int i = 0; i < stats.length; i++) {
+            SendStatus status = stats[i];
 
-        if (this.session.getDebug()) {
-            this.session.getDebugOut().println(line);
+            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;
+    }
 
-        if (line.isError()) {
-            return false;
+    /**
+     * 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;
+    }
 
-        // now the data...  I could look at the type, but
-        try {
-            OutputStream os = s.getOutputStream();
+    /**
+     * 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();
 
-            // todo - be smarter here and send in chunks to let the other side
-            // digest easier.  There's a 3 min recommended timeout per chunk...
+        // 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]);
+                }
+            }
+        }
 
-            msg.writeTo(os);
-            os.flush();
-        } catch (IOException e) {
-            throw new SMTPTransportException(e);
-        } catch (MessagingException e) {
-            throw new SMTPTransportException(e);
+        // 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);
+    }
 
-        // now to finish
-        sendLine(s, "");
-        sendLine(s, ".");
+    /**
+     * 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);
+    }
 
-        line = new SMTPReply(receiveLine(s, 10 * MIN_MILLIS));
+    /**
+     * 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;
+    }
 
-        return !line.isError();
+    /**
+     * 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);
     }
 
     /**
-     * Sends the QUIT message and receieves the response
+     * 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 boolean sendQuit(Socket s) throws SMTPTransportException, MalformedSMTPReplyException {
-        sendLine(s, "QUIT");
+    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;
+    }
 
-        SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
+    /**
+     * 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);
+    }
 
-        return !line.isError();
+    /**
+     * 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);
     }
 
     /**
-     * Sets a receiver address for the current message
+     * 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 sendRcptTo(Socket s, String addr) throws SMTPTransportException, MalformedSMTPReplyException {
-        if (addr == null || "".equals(addr)) {
-            throw new SMTPTransportException("invalid address");
+    protected boolean isSessionPropertyTrue(String name) {
+        String property = session.getProperty(name);
+        if (property != null) {
+            return property.equals("true");
         }
+        return false;
+    }
 
-        String msg = "RCPT TO: " + fixEmailAddress(addr);
+    /**
+     * 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;
+    }
 
-        sendLine(s, msg);
+    /**
+     * 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);
+    }
 
-        SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
+    /**
+     * Close the server connection at termination.
+     */
+    protected void closeServerConnection() {
+        try {
+            socket.close();
+        } catch (IOException ignored) {
+        }
 
-        return !line.isError();
+        socket = null;
+        inputStream = null;
+        outputStream = null;
     }
 
     /**
-     * Set the sender for this mail.
+     * Creates a connected socket
+     * 
+     * @exception MessagingException
      */
-    protected boolean sendMailFrom(Socket s, Address[] from) throws SMTPTransportException, MalformedSMTPReplyException {
-        if (from == null || from.length == 0) {
-            throw new SMTPTransportException("no FROM address");
+    protected void getConnectedSocket() throws IOException {
+        if (debug) {
+            debugOut("Attempting plain socket connection to server " + host + ":" + port);
         }
 
-        // TODO - what do we do w/ more than one from???
-        String msg = "MAIL FROM: " + fixEmailAddress(from[0].toString());
+        // 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);
 
-        sendLine(s, msg);
+        // 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);
+        }
 
-        SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
+        // check for a local port...default is to allow socket to choose.
+        int localPort = getIntProtocolProperty(MAIL_SMTP_LOCALPORT, 0);
 
-        return !line.isError();
+        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);
+        }
     }
 
     /**
-     * Sends the initiating "HELO" message.  We're keeping it simple, just
-     * identifying ourselves as we dont' require service extensions, and
-     * want to keep it simple for now
-     *
-     * @param s socket we are talking on.  It's assumed to be open and in
-     * right state for this message
+     * Creates a connected SSL socket for an initial SSL connection.
+     * 
+     * @exception MessagingException
      */
-    protected boolean sendHelo(Socket s) throws SMTPTransportException, MalformedSMTPReplyException {
-        String fqdm = null;
+    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);
+                    }
 
-        try {
-            fqdm = InetAddress.getLocalHost().getHostName();
-        } catch (UnknownHostException e) {
-            // fine, we're misconfigured - ignore
-        }
+                    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 (fqdm == null) {
-            fqdm = session.getProperty(MAIL_SMTP_LOCALHOST);
+                        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 (fqdm == null) {
-            throw new SMTPTransportException("Can't get local hostname. " +
-                    " Please correctly configure JDK/DNS or set mail.smtp.localhost");
+        if (timeout >= 0) {
+            socket.setSoTimeout(timeout);
         }
+    }
 
-        sendLine(s, "HELO " + fqdm);
+    /**
+     * 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 {
 
-        SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
+            // 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();
+            }
 
-        return !line.isError();
+            // 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(Socket s) throws SMTPTransportException, MalformedSMTPReplyException {
-        SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS));
+    protected boolean getWelcome() throws MessagingException {
+        SMTPReply line = getReply();
         return !line.isError();
     }
 
     /**
-     * Sends a  message down the socket and terminates with the
-     * appropriate CRLF
+     * 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 sendLine(Socket s, String data) throws SMTPTransportException {
-        if (s == null) {
-            throw new SMTPTransportException("bonehead...");
+    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);
         }
 
-        if (!s.isConnected()) {
-            throw new SMTPTransportException("no connection");
+        // 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 {
-            OutputStream out = s.getOutputStream();
+            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);
+        }
 
-            out.write(data.getBytes());
-            out.write(CR);
-            out.write(LF);
-            out.flush();
+        // if not there, see if we have something in the message header.
+        if (from == null || from.length() == 0) {
+            Address[] fromAddresses = message.getFrom();
 
-            if (this.session.getDebug()) {
-                this.session.getDebugOut().println("sent: " + data);
+            // 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 SMTPTransportException(e);
+            throw new MessagingException(e.toString());
         }
     }
 
     /**
-     * Receives one line from the server.  A line is a sequence of bytes
+     * Receives one line from the server. A line is a sequence of bytes
      * terminated by a CRLF
-     *
-     * @param s socket to receive from
+     * 
      * @return the line from the server as String
      */
-    protected String receiveLine(Socket s, int delayMillis) throws SMTPTransportException {
-        if (s == null) {
-            throw new SMTPTransportException("bonehead...");
+    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;
+    }
 
-        if (!s.isConnected()) {
-            throw new SMTPTransportException("no connection");
+    /**
+     * 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 = s.getSoTimeout();
+            timeout = socket.getSoTimeout();
 
-            s.setSoTimeout(delayMillis);
-
-            InputStream is = s.getInputStream();
+            socket.setSoTimeout(delayMillis);
 
             StringBuffer buff = new StringBuffer();
 
             int c;
             boolean crFound = false, lfFound = false;
 
-            while ((c = is.read()) != -1 && crFound == false && lfFound == false) {
-                buff.append((char) c);
-
+            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;
-                }
-                if (c == LF) {
+                } else if (c == LF) {
                     lfFound = true;
+                } else {
+                    buff.append((char) c);
                 }
             }
 
-            if (this.session.getDebug()) {
-                this.session.getDebugOut().println("received : " + buff.toString());
-            }
+            String line = buff.toString();
+            return line;
 
-            return buff.toString();
         } catch (SocketException e) {
-            throw new SMTPTransportException(e);
+            throw new MessagingException(e.toString());
         } catch (IOException e) {
-            throw new SMTPTransportException(e);
+            throw new MessagingException(e.toString());
         } finally {
             try {
-                s.setSoTimeout(timeout);
+                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;
+    }
 
     /**
-     * Creates and returns a connected socket
+     * 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 Socket getConnectedSocket() throws MessagingException {
-        Socket s = new Socket();
+    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;
+        }
 
-        String mail_host = this.session.getProperty(MAIL_HOST);
+        // get a fresh extension mapping table.
+        serverExtensionArgs = new HashMap();
 
-        if (mail_host == null || "".equals(mail_host)) {
-            mail_host = DEFAULT_MAIL_HOST;
+        // 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;
+    }
 
-        String portString = this.session.getProperty(MAIL_SMTP_PORT);
+    /**
+     * Send the HELO command to the SMTP server.
+     * 
+     * @exception MessagingException
+     */
+    protected void sendHelo() throws MessagingException {
+        sendLine("HELO " + getLocalHost());
 
-        int port = DEFAULT_MAIL_SMTP_PORT;
+        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");
+        }
+    }
 
-        if (portString != null && !"".equals(portString)) {
+    /**
+     * 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 {
-                port = Integer.parseInt(portString);
-            } catch (NumberFormatException e) {
-                // ignore - we don't care, leave as default
+                localHost = InetAddress.getLocalHost().getHostName();
+            } catch (UnknownHostException e) {
+                // fine, we're misconfigured - ignore
             }
-        }
 
-        try {
-            if (this.session.getDebug()) {
-                this.session.getDebugOut().println("connecting to " + mail_host);
+            if (localHost == null) {
+                localHost = getProtocolProperty(MAIL_SMTP_LOCALHOST);
             }
 
-            s.connect(new InetSocketAddress(mail_host, port));
+            if (localHost == null) {
+                localHost = getSessionProperty(MAIL_LOCALHOST);
+            }
 
-            if (this.session.getDebug()) {
-                this.session.getDebugOut().println("connected to " + mail_host);
+            if (localHost == null) {
+                throw new MessagingException("Can't get local hostname. "
+                        + " Please correctly configure JDK/DNS or set mail.smtp.localhost");
             }
-        } catch (IOException e) {
-            if (this.session.getDebug()) {
-                this.session.getDebugOut().println("error connecting to " + mail_host);
+        }
+
+        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;
+        }
+    }
 
-            throw new MessagingException("Error connecting to " + mail_host, e);
+    /**

[... 303 lines stripped ...]