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 ...]