Return-Path: Delivered-To: apmail-geronimo-scm-archive@www.apache.org Received: (qmail 27368 invoked from network); 6 Feb 2006 21:17:10 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (209.237.227.199) by minotaur.apache.org with SMTP; 6 Feb 2006 21:17:10 -0000 Received: (qmail 44815 invoked by uid 500); 6 Feb 2006 21:17:09 -0000 Delivered-To: apmail-geronimo-scm-archive@geronimo.apache.org Received: (qmail 44783 invoked by uid 500); 6 Feb 2006 21:17:09 -0000 Mailing-List: contact scm-help@geronimo.apache.org; run by ezmlm Precedence: bulk list-help: list-unsubscribe: List-Post: Reply-To: dev@geronimo.apache.org List-Id: Delivered-To: mailing list scm@geronimo.apache.org Received: (qmail 44772 invoked by uid 99); 6 Feb 2006 21:17:09 -0000 Received: from asf.osuosl.org (HELO asf.osuosl.org) (140.211.166.49) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 06 Feb 2006 13:17:09 -0800 X-ASF-Spam-Status: No, hits=-9.4 required=10.0 tests=ALL_TRUSTED,NO_REAL_NAME X-Spam-Check-By: apache.org Received: from [209.237.227.194] (HELO minotaur.apache.org) (209.237.227.194) by apache.org (qpsmtpd/0.29) with SMTP; Mon, 06 Feb 2006 13:17:05 -0800 Received: (qmail 26998 invoked by uid 65534); 6 Feb 2006 21:16:44 -0000 Message-ID: <20060206211644.26997.qmail@minotaur.apache.org> Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r375375 [2/3] - in /geronimo/trunk/modules/javamail-transport/src: java/org/apache/geronimo/javamail/authentication/ java/org/apache/geronimo/javamail/transport/smtp/ resources/META-INF/ Date: Mon, 06 Feb 2006 21:16:32 -0000 To: scm@geronimo.apache.org From: bsnyder@apache.org X-Mailer: svnmailer-1.0.6 X-Virus-Checked: Checked by ClamAV on apache.org X-Spam-Rating: minotaur.apache.org 1.6.2 0/1000/N Modified: geronimo/trunk/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java URL: http://svn.apache.org/viewcvs/geronimo/trunk/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java?rev=375375&r1=375374&r2=375375&view=diff ============================================================================== --- geronimo/trunk/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java (original) +++ geronimo/trunk/modules/javamail-transport/src/java/org/apache/geronimo/javamail/transport/smtp/SMTPTransport.java Mon Feb 6 13:16:28 2006 @@ -20,17 +20,38 @@ 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.mail.util.Base64; +import org.apache.geronimo.mail.util.XText; /** * Simple implementation of SMTP transport. Just does plain RFC821-ish @@ -53,111 +74,1176 @@ * @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_HOST = "mail.host"; + protected static final String MAIL_LOCALHOST = "mail.localhost"; + protected static final String MAIL_SSLFACTORY_CLASS = "mail.SSLSocketFactory.class"; - 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; /** - * @param session - * @param name + * 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 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 transport that created us...this is the source of configuration information. + protected SMTPTransport transport; + + // 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(); } - 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 { - // don't bother me w/ null messages or no addreses - if (message == null) { - throw new MessagingException("Null message"); + /** + * 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); } - if (addresses == null || addresses.length == 0) { - throw new MessagingException("Null or empty address array"); + // 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; } - SendStatus[] stat = new SendStatus[addresses.length]; + // 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. - Socket s = getConnectedSocket(); + getConnection(host, port, username, password); // receive welcoming message - if (!getWelcome(s)) { + if (!getWelcome()) { throw new MessagingException("Error in getting welcome msg"); } // say hello - if (!sendHelo(s)) { - throw new MessagingException("Error in saying HELO to server"); + 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); + } catch (SMTPTransportException e) { + if (debug) { + debugOut("Exception establishing connection", e); + } + throw new MessagingException("error", e); + } catch (MalformedSMTPReplyException e) { + if (debug) { + debugOut("Exception establishing connection", e); + } + throw new MessagingException("error", e); + } + return true; + } + + /** + * Send a message to multiple addressees. + * + * @param message The message we're sending. + * @param addresses An array of addresses to send to. + * + * @exception MessagingException + */ + public void sendMessage(Message message, Address[] addresses) throws MessagingException { + if (!isConnected()) { + throw new IllegalStateException("Not connected"); + } + // don't bother me w/ null messages or no addreses + if (message == null) { + throw new MessagingException("Null message"); + } + + // SMTP only handles instances of MimeMessage, not the more general message case. + if (!(message instanceof MimeMessage)) { + throw new MessagingException("SMTP can only send MimeMessages"); + } + + // we must have a message list. + if (addresses == null || addresses.length == 0) { + throw new MessagingException("Null or empty address array"); + } + + boolean haveGroup = false; + + // enforce the requirement that all of the targets are InternetAddress instances. + for (int i = 0; i < addresses.length; i++) { + if (addresses[i] instanceof InternetAddress) { + // and while we're here, see if we have a groups in the address list. If we do, then + // we're going to need to expand these before sending. + if (((InternetAddress)addresses[i]).isGroup()) { + haveGroup = true; + } + } + else { + throw new MessagingException("Illegal InternetAddress " + addresses[i]); + } + } + + // did we find a group? Time to expand this into our full target list. + if (haveGroup) { + addresses = expandGroups(addresses); + } + + + SendStatus[] stats = new SendStatus[addresses.length]; + + // create our lists for notification and exception reporting. + Address[] sent = null; + Address[] unsent = null; + Address[] invalid = null; + + try { + // send sender first. If this failed, send a failure notice of the event, using the full list of + // addresses as the unsent, and nothing for the rest. + if (!sendMailFrom(message)) + { + unsent = addresses; + sent = new Address[0]; + invalid = new Address[0]; + // notify of the error. + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); + + // include the reponse information here. + SMTPReply last = lastServerResponse; + // now send an "uber-exception" to indicate the failure. + throw new SMTPSendFailedException("MAIL FROM", last.getCode(), last.getMessage(), null, sent, unsent, invalid); } + String dsn = null; + + // there's an optional notification argument that can be added to MAIL TO. See if we've been + // provided with one. - // send sender - if (!sendMailFrom(s, message.getFrom())) { - throw new MessagingException("Error in setting the MAIL FROM"); + // an SMTPMessage object is the first source + if (message instanceof SMTPMessage) { + // get the notification options + int options = ((SMTPMessage)message).getNotifyOptions(); + + switch (options) { + // a zero value indicates nothing is set. + case 0: + break; + + case SMTPMessage.NOTIFY_NEVER: + dsn = "NEVER"; + break; + + case SMTPMessage.NOTIFY_SUCCESS: + dsn = "SUCCESS"; + break; + + case SMTPMessage.NOTIFY_FAILURE: + dsn = "FAILURE"; + break; + + case SMTPMessage.NOTIFY_DELAY: + dsn = "DELAY"; + break; + + // now for combinations...there are few enough combinations here + // that we can just handle this in the switch statement rather than have to + // concatentate everything together. + case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE): + dsn = "SUCCESS,FAILURE"; + break; + + case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_DELAY): + dsn = "SUCCESS,DELAY"; + break; + + case (SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): + dsn = "FAILURE,DELAY"; + break; + + case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY): + dsn = "SUCCESS,FAILURE,DELAY"; + break; + } + } + + // if still null, grab a property value (yada, yada, yada...) + if (dsn == null) { + dsn = getProtocolProperty(MAIL_SMTP_DSN_NOTIFY); } - // send recipients. Only send if not null or "", and just ignore (but log) any errors + // we need to know about any failures once we've gone through the complete list, so keep a + // failure flag. + boolean sendFailure = false; + + // event notifcation requires we send lists of successes and failures broken down by category. + // The categories are: + // + // 1) addresses successfully processed. + // 2) addresses deemed valid, but had a processing failure that prevented sending. + // 3) addressed deemed invalid (basically all other processing failures). + ArrayList sentAddresses = new ArrayList(); + ArrayList unsentAddresses = new ArrayList(); + ArrayList invalidAddresses = new ArrayList(); + + // Now we add a MAIL TO record for each recipient. At this point, we just collect for (int i = 0; i < addresses.length; i++) { - String to = addresses[i].toString(); + InternetAddress target = (InternetAddress)addresses[i]; - int status = SendStatus.SUCCESS; + // write out the record now. + SendStatus status = sendRcptTo(target, dsn); + stats[i] = status; + + switch (status.getStatus()) { + // successfully sent + case SendStatus.SUCCESS: + sentAddresses.add(target); + break; + + // we have an invalid address of some sort, or a general sending error (which we'll + // interpret as due to an invalid address. + case SendStatus.INVALID_ADDRESS: + case SendStatus.GENERAL_ERROR: + sendFailure = true; + invalidAddresses.add(target); + break; + + // good address, but this was a send failure. + case SendStatus.SEND_FAILURE: + sendFailure = true; + unsentAddresses.add(target); + break; + } + } - if (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 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(); + } - status = SendStatus.FAIL; - } - } else { - status = SendStatus.FAIL; + // if still false on the message object, check for a property version also + if (!partialSends) { + partialSends = isProtocolPropertyTrue(MAIL_SMTP_SENDPARTIAL); } - stat[i] = new SendStatus(status, 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]); + + // notify of the error. + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); + + // go reset our connection so we can process additional sends. + resetConnection(); + + // get a list of chained exceptions for all of the failures. + MessagingException failures = generateExceptionChain(stats, false); + + // now send an "uber-exception" to indicate the failure. + throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); + } } + // 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]); + // send data - if (!sendData(s, message)) { + if (!sendData(message)) { throw new MessagingException("Error sending data"); } + + // if sendFailure is true, we had an error during the address phase, but we had permission to + // process this as a partial send operation. Now that the data has been sent ok, it's time to + // report the partial failure. + if (sendFailure) { + // notify our listeners of the partial delivery. + notifyTransportListeners(TransportEvent.MESSAGE_PARTIALLY_DELIVERED, sent, unsent, invalid, message); + + // get a list of chained exceptions for all of the failures (and the successes, if reportSuccess has been + // turned on). + MessagingException failures = generateExceptionChain(stats, getReportSuccess()); + + // now send an "uber-exception" to indicate the failure. + throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid); + } + + // notify our listeners of successful delivery. + notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, sent, unsent, invalid, message); + + // we've not had any failures, but we've been asked to report success as an exception. Do + // this now. + if (reportSuccess) { + // generate the chain of success exceptions (we already know there are no failure ones to report). + MessagingException successes = generateExceptionChain(stats, reportSuccess); + if (successes != null) { + throw successes; + } + } + } catch (SMTPSendFailedException e) { + // if this is a send failure, we've already handled notifications....just rethrow it. + throw e; + } catch (MessagingException e) { + // notify of the error. + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message); + throw e; + } + } + + + /** + * Close the connection. On completion, we'll be disconnected from + * the server and unable to send more data. + * + * @exception MessagingException + */ + public void close() throws MessagingException { + // if we're already closed, get outta here. + if (socket == null) { + return; + } + try { // say goodbye - sendQuit(s); + sendQuit(); + } finally { + // and close up the connection. We do this in a finally block to make sure the connection + // is shut down even if quit gets an error. + closeServerConnection(); + } + } + + + /** + * Turn a series of send status items into a chain of exceptions + * indicating the state of each send operation. + * + * @param stats The list of SendStatus items. + * @param reportSuccess + * Indicates whether we should include the report success items. + * + * @return The head of a chained list of MessagingExceptions. + */ + protected MessagingException generateExceptionChain(SendStatus[] stats, boolean reportSuccess) { + MessagingException current = null; + + for (int i = 0; i < stats.length; i++) { + SendStatus status = stats[i]; + + if (status != null) { + MessagingException nextException = stats[i].getException(reportSuccess); + // if there's an exception associated with this status, chain it up with the rest. + if (nextException != null) { + if (current == null) { + current = nextException; + } + else { + current.setNextException(nextException); + current = nextException; + } + } + } + } + return current; + } + + + /** + * Reset the server connection after an error. + * + * @exception MessagingException + */ + protected void resetConnection() throws MessagingException { + // we want the caller to retrieve the last response responsbile for requiring the reset, so save and + // restore that info around the reset. + SMTPReply last = lastServerResponse; + + // send a reset command. + SMTPReply line = sendCommand("RSET"); + + // if this did not reset ok, just close the connection + if (line.getCode() != COMMAND_ACCEPTED) { + close(); + } + // restore this. + lastServerResponse = last; + } + + + /** + * Expand the address list by converting any group addresses + * into single address targets. + * + * @param addresses The input array of addresses. + * + * @return The expanded array of addresses. + * @exception MessagingException + */ + protected Address[] expandGroups(Address[] addresses) throws MessagingException { + ArrayList expandedAddresses = new ArrayList(); + // run the list looking for group addresses, and add the full group list to our targets. + for (int i = 0; i < addresses.length; i++) { + InternetAddress address = (InternetAddress)addresses[i]; + // not a group? Just copy over to the other list. + if (!address.isGroup()) { + expandedAddresses.add(address); + } + else { + // get the group address and copy each member of the group into the expanded list. + InternetAddress[] groupAddresses = address.getGroup(true); + for (int j = 1; j < groupAddresses.length; j++) { + expandedAddresses.add(groupAddresses[j]); + } + } + } + + // convert back into an array. + return (Address [])expandedAddresses.toArray(new Address[0]); + } + + + /** + * Create a transport connection object and connect it to the + * target server. + * + * @param host The target server host. + * @param port The connection port. + * + * @exception MessagingException + */ + protected void getConnection(String host, int port, String username, String password) throws IOException + { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + // and see if STARTTLS is enabled. + useTLS = isProtocolPropertyTrue(MAIL_SMTP_TLS); + serverAuthenticationMechanisms = new HashMap(); + // We might have been passed a socket to connect with...if not, we need to create one of the correct type. + if (socket == null) { + // if this is the "smtps" protocol, we start with an SSLSocket + if (sslConnection) { + getConnectedSSLSocket(); + } + else + { + getConnectedSocket(); + } + } + // if we already have a socket, get some information from it and override what we've been passed. + else { + port = socket.getPort(); + host = socket.getInetAddress().getHostName(); + } + // now set up the input/output streams. + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + } + + /** + * Get a property associated with this mail protocol. + * + * @param name The name of the property. + * + * @return The property value (returns null if the property has not been set). + */ + protected String getProtocolProperty(String name) { + // the name we're given is the least qualified part of the name. We construct the full property name + // using the protocol (either "smtp" or "smtps"). + String fullName = "mail." + protocol + "." + name; + return getSessionProperty(fullName); + } + + /** + * Get a property associated with this mail session. + * + * @param name The name of the property. + * + * @return The property value (returns null if the property has not been set). + */ + protected String getSessionProperty(String name) { + return session.getProperty(name); + } + + /** + * Get a property associated with this mail session. Returns + * the provided default if it doesn't exist. + * + * @param name The name of the property. + * @param defaultValue + * The default value to return if the property doesn't exist. + * + * @return The property value (returns defaultValue if the property has not been set). + */ + protected String getSessionProperty(String name, String defaultValue) { + String result = session.getProperty(name); + if (result == null) { + return defaultValue; + } + return result; + } + + /** + * Get a property associated with this mail session. Returns + * the provided default if it doesn't exist. + * + * @param name The name of the property. + * @param defaultValue + * The default value to return if the property doesn't exist. + * + * @return The property value (returns defaultValue if the property has not been set). + */ + protected String getProtocolProperty(String name, String defaultValue) { + // the name we're given is the least qualified part of the name. We construct the full property name + // using the protocol (either "smtp" or "smtps"). + String fullName = "mail." + protocol + "." + name; + return getSessionProperty(fullName, defaultValue); + } + + /** + * Get a property associated with this mail session as an integer value. Returns + * the default value if the property doesn't exist or it doesn't have a valid int value. + * + * @param name The name of the property. + * @param defaultValue + * The default value to return if the property doesn't exist. + * + * @return The property value converted to an int. + */ + protected int getIntSessionProperty(String name, int defaultValue) { + String result = getSessionProperty(name); + if (result != null) { try { - s.close(); - } catch (IOException ignored) { + // convert into an int value. + return Integer.parseInt(result); + } catch (NumberFormatException e) { } - } catch (SMTPTransportException e) { - throw new MessagingException("error", e); - } catch (MalformedSMTPReplyException e) { - throw new MessagingException("error", e); } + // return default value if it doesn't exist is isn't convertable. + return defaultValue; + } + + /** + * Get a property associated with this mail session as an integer value. Returns + * the default value if the property doesn't exist or it doesn't have a valid int value. + * + * @param name The name of the property. + * @param defaultValue + * The default value to return if the property doesn't exist. + * + * @return The property value converted to an int. + */ + protected int getIntProtocolProperty(String name, int defaultValue) { + // the name we're given is the least qualified part of the name. We construct the full property name + // using the protocol (either "smtp" or "smtps"). + String fullName = "mail." + protocol + "." + name; + return getIntSessionProperty(fullName, defaultValue); + } + + + /** + * Process a session property as a boolean value, returning + * either true or false. + * + * @return True if the property value is "true". Returns false for any + * other value (including null). + */ + protected boolean isProtocolPropertyTrue(String name) { + // the name we're given is the least qualified part of the name. We construct the full property name + // using the protocol (either "smtp" or "smtps"). + String fullName = "mail." + protocol + "." + name; + return isSessionPropertyTrue(fullName); + } + + /** + * Process a session property as a boolean value, returning + * either true or false. + * + * @return True if the property value is "true". Returns false for any + * other value (including null). + */ + protected boolean isSessionPropertyTrue(String name) { + String property = session.getProperty(name); + if (property != null) { + return property.equals("true"); + } + return false; + } + + /** + * Process a session property as a boolean value, returning + * either true or false. + * + * @return True if the property value is "false". Returns false for + * other value (including null). + */ + protected boolean isSessionPropertyFalse(String name) { + String property = session.getProperty(name); + if (property != null) { + return property.equals("false"); + } + return false; + } + + /** + * Process a session property as a boolean value, returning + * either true or false. + * + * @return True if the property value is "false". Returns false for + * other value (including null). + */ + protected boolean isProtocolPropertyFalse(String name) { + // the name we're given is the least qualified part of the name. We construct the full property name + // using the protocol (either "smtp" or "smtps"). + String fullName = "mail." + protocol + "." + name; + return isSessionPropertyTrue(fullName); + } + + + /** + * Close the server connection at termination. + */ + protected void closeServerConnection() + { + try { + socket.close(); + } catch (IOException ignored) { + } + + socket = null; + inputStream = null; + outputStream = null; + } + + + /** + * Creates a connected socket + * + * @exception MessagingException + */ + protected void getConnectedSocket() throws IOException { + if (debug) { + debugOut("Attempting plain socket connection to server " + host + ":" + port); + } + + + // the socket factory can be specified via a session property. By default, we just directly + // instantiate a socket without using a factor. + String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS); + + // there are several protocol properties that can be set to tune the created socket. We need to + // retrieve those bits before creating the socket. + int timeout = getIntProtocolProperty(MAIL_SMTP_TIMEOUT, -1); + InetAddress localAddress = null; + // see if we have a local address override. + String localAddrProp = getProtocolProperty(MAIL_SMTP_LOCALADDRESS); + if (localAddrProp != null) { + localAddress = InetAddress.getByName(localAddrProp); + } + + // check for a local port...default is to allow socket to choose. + int localPort = getIntProtocolProperty(MAIL_SMTP_LOCALPORT, 0); + + socket = null; + + // if there is no socket factory defined (normal), we just create a socket directly. + if (socketFactory == null) { + socket = new Socket(host, port, localAddress, localPort); + } + + else { + try { + int socketFactoryPort = getIntProtocolProperty(MAIL_SMTP_FACTORY_PORT, -1); + + // we choose the port used by the socket based on overrides. + Integer portArg = new Integer(socketFactoryPort == -1 ? port : socketFactoryPort); + + // use the current context loader to resolve this. + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Class factoryClass = loader.loadClass(socketFactory); + + // done indirectly, we need to invoke the method using reflection. + // This retrieves a factory instance. + Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); + Object defFactory = getDefault.invoke(new Object(), new Object[0]); + + // now that we have the factory, there are two different createSocket() calls we use, + // depending on whether we have a localAddress override. + + if (localAddress != null) { + // retrieve the createSocket(String, int, InetAddress, int) method. + Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE }; + Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); + + Object[] createSocketArgs = new Object[] { host, portArg, localAddress, new Integer(localPort) }; + socket = (Socket)createSocket.invoke(defFactory, createSocketArgs); + } + else { + // retrieve the createSocket(String, int) method. + Class[] createSocketSig = new Class[] { String.class, Integer.TYPE }; + Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); + + Object[] createSocketArgs = new Object[] { host, portArg }; + socket = (Socket)createSocket.invoke(defFactory, createSocketArgs); + } + } catch (Throwable e) { + // if a socket factor is specified, then we may need to fall back to a default. This behavior + // is controlled by (surprise) more session properties. + if (isProtocolPropertyTrue(MAIL_SMTP_FACTORY_FALLBACK)) { + if (debug) { + debugOut("First plain socket attempt faile, falling back to default factory", e); + } + socket = new Socket(host, port, localAddress, localPort); + } + // we have an exception. We're going to throw an IOException, which may require unwrapping + // or rewrapping the exception. + else { + // we have an exception from the reflection, so unwrap the base exception + if (e instanceof InvocationTargetException) { + e = ((InvocationTargetException)e).getTargetException(); + } + + if (debug) { + debugOut("Plain socket creation failure", e); + } + + // throw this as an IOException, with the original exception attached. + IOException ioe = new IOException("Error connecting to " + host + ", " + port); + ioe.initCause(e); + throw ioe; + } + } + } + + if (timeout >= 0) { + socket.setSoTimeout(timeout); + } + } + + + /** + * Creates a connected SSL socket for an initial SSL connection. + * + * @exception MessagingException + */ + protected void getConnectedSSLSocket() throws IOException { + if (debug) { + debugOut("Attempting SSL socket connection to server " + host + ":" + port); + } + // the socket factory can be specified via a protocol property, a session property, and if all else + // fails (which it usually does), we fall back to the standard factory class. + String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS, getSessionProperty(MAIL_SSLFACTORY_CLASS, "javax.net.ssl.SSLSocketFactory")); + + // there are several protocol properties that can be set to tune the created socket. We need to + // retrieve those bits before creating the socket. + int timeout = getIntProtocolProperty(MAIL_SMTP_TIMEOUT, -1); + InetAddress localAddress = null; + // see if we have a local address override. + String localAddrProp = getProtocolProperty(MAIL_SMTP_LOCALADDRESS); + if (localAddrProp != null) { + localAddress = InetAddress.getByName(localAddrProp); + } + + // check for a local port...default is to allow socket to choose. + int localPort = getIntProtocolProperty(MAIL_SMTP_LOCALPORT, 0); + + socket = null; + + // if there is no socket factory defined (normal), we just create a socket directly. + if (socketFactory == null) { + socket = new Socket(host, port, localAddress, localPort); + } + + else { + // we'll try this with potentially two different factories if we're allowed to fall back. + boolean fallback = isProtocolPropertyTrue(MAIL_SMTP_FACTORY_FALLBACK); + + while (true) { + try { + if (debug) { + debugOut("Creating SSL socket using factory " + socketFactory); + } + + int socketFactoryPort = getIntProtocolProperty(MAIL_SMTP_FACTORY_PORT, -1); + + // we choose the port used by the socket based on overrides. + Integer portArg = new Integer(socketFactoryPort == -1 ? port : socketFactoryPort); + + // use the current context loader to resolve this. + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Class factoryClass = loader.loadClass(socketFactory); + + // done indirectly, we need to invoke the method using reflection. + // This retrieves a factory instance. + Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); + Object defFactory = getDefault.invoke(new Object(), new Object[0]); + + // now that we have the factory, there are two different createSocket() calls we use, + // depending on whether we have a localAddress override. + + if (localAddress != null) { + // retrieve the createSocket(String, int, InetAddress, int) method. + Class[] createSocketSig = new Class[] { String.class, Integer.TYPE, InetAddress.class, Integer.TYPE }; + Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); + + Object[] createSocketArgs = new Object[] { host, portArg, localAddress, new Integer(localPort) }; + socket = (Socket)createSocket.invoke(defFactory, createSocketArgs); + } + else { + // retrieve the createSocket(String, int) method. + Class[] createSocketSig = new Class[] { String.class, Integer.TYPE }; + Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); + + Object[] createSocketArgs = new Object[] { host, portArg }; + socket = (Socket)createSocket.invoke(defFactory, createSocketArgs); + } + } catch (Throwable e) { + // if we're allowed to fallback, then use the default factory and try this again. We only + // allow this to happen once. + if (fallback) { + if (debug) { + debugOut("First attempt at creating SSL socket failed, falling back to default factory"); + } + socketFactory = "javax.net.ssl.SSLSocketFactory"; + fallback = false; + continue; + } + // we have an exception. We're going to throw an IOException, which may require unwrapping + // or rewrapping the exception. + else { + // we have an exception from the reflection, so unwrap the base exception + if (e instanceof InvocationTargetException) { + e = ((InvocationTargetException)e).getTargetException(); + } + + if (debug) { + debugOut("Failure creating SSL socket", e); + } + + // throw this as an IOException, with the original exception attached. + IOException ioe = new IOException("Error connecting to " + host + ", " + port); + ioe.initCause(e); + throw ioe; + } + } + } + } + + if (timeout >= 0) { + socket.setSoTimeout(timeout); + } + } + + + /** + * Switch the connection to using TLS level security, + * switching to an SSL socket. + */ + protected void getConnectedTLSSocket() throws MessagingException { + if (debug) { + debugOut("Attempting to negotiate STARTTLS with server " + host); + } + // tell the server of our intention to start a TLS session + SMTPReply line = sendCommand("STARTTLS"); + + if (line.getCode() != SERVICE_READY) { + if (debug) { + debugOut("STARTTLS command rejected by SMTP server " + host); + } + throw new MessagingException("Unable to make TLS server connection"); + } + // it worked, now switch the socket into TLS mode + try { + + // we use the same target and port as the current connection. + String host = socket.getInetAddress().getHostName(); + int port = socket.getPort(); + + // the socket factory can be specified via a session property. By default, we use + // the native SSL factory. + String socketFactory = getProtocolProperty(MAIL_SMTP_FACTORY_CLASS, "javax.net.ssl.SSLSocketFactory"); + + // use the current context loader to resolve this. + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Class factoryClass = loader.loadClass(socketFactory); + + // done indirectly, we need to invoke the method using reflection. + // This retrieves a factory instance. + Method getDefault = factoryClass.getMethod("getDefault", new Class[0]); + Object defFactory = getDefault.invoke(new Object(), new Object[0]); + + // now we need to invoke createSocket() + Class[] createSocketSig = new Class[] { Socket.class, String.class, Integer.TYPE, Boolean.TYPE }; + Method createSocket = factoryClass.getMethod("createSocket", createSocketSig); + + Object[] createSocketArgs = new Object[] { socket, host, new Integer(port), Boolean.TRUE }; + + // and finally create the socket + Socket sslSocket = (Socket)createSocket.invoke(defFactory, createSocketArgs); + + // if this is an instance of SSLSocket (very common), try setting the protocol to be + // "TLSv1". If this is some other class because of a factory override, we'll just have to + // accept that things will work. + if (sslSocket instanceof SSLSocket) { + ((SSLSocket)sslSocket).setEnabledProtocols(new String[] {"TLSv1"} ); + ((SSLSocket)sslSocket).setUseClientMode(true); + ((SSLSocket)sslSocket).startHandshake(); + } + + + // and finally, as a last step, replace our input streams with the secure ones. + inputStream = sslSocket.getInputStream(); + outputStream = sslSocket.getOutputStream(); + // 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); + } + } - return stat; + /** + * Get the servers welcome blob from the wire.... + */ + protected boolean getWelcome() throws MessagingException { + SMTPReply line = getReply(); + return !line.isError(); } /** @@ -165,19 +1251,10 @@ * server is in the right place and ready for getting the DATA message * and the data right place in the sequence */ - protected boolean sendData(Socket s, Message msg) throws SMTPTransportException, MalformedSMTPReplyException { - if (msg == null) { - throw new SMTPTransportException("invalid message"); - } + protected boolean sendData(Message msg) throws MessagingException { // send the DATA command - sendLine(s, "DATA"); - - SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); - - if (this.session.getDebug()) { - this.session.getDebugOut().println(line); - } + SMTPReply line = sendCommand("DATA"); if (line.isError()) { return false; @@ -185,24 +1262,20 @@ // now the data... I could look at the type, but try { - OutputStream os = s.getOutputStream(); - - // todo - be smarter here and send in chunks to let the other side - // digest easier. There's a 3 min recommended timeout per chunk... - - msg.writeTo(os); - os.flush(); + msg.writeTo(outputStream); + outputStream.flush(); } catch (IOException e) { throw new SMTPTransportException(e); } catch (MessagingException e) { throw new SMTPTransportException(e); } - // now to finish - sendLine(s, ""); - sendLine(s, "."); + // now to finish, we send a CRLF sequence, followed by a ".". + sendLine(""); + sendLine("."); - line = new SMTPReply(receiveLine(s, 10 * MIN_MILLIS)); + // use a longer time out here to give the server time to process the data. + line = new SMTPReply(receiveLine(TIMEOUT * 2)); return !line.isError(); } @@ -210,114 +1283,245 @@ /** * Sends the QUIT message and receieves the response */ - protected boolean sendQuit(Socket s) throws SMTPTransportException, MalformedSMTPReplyException { - sendLine(s, "QUIT"); - - SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); - - return !line.isError(); + 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 boolean sendRcptTo(Socket s, String addr) throws SMTPTransportException, MalformedSMTPReplyException { - if (addr == null || "".equals(addr)) { - throw new SMTPTransportException("invalid address"); + 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); } - - String msg = "RCPT TO: " + fixEmailAddress(addr); - - sendLine(s, msg); - - SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); - - return !line.isError(); } /** * Set the sender for this mail. + * + * @param message The message we're sending. + * + * @exception MessagingException */ - protected boolean sendMailFrom(Socket s, Address[] from) throws SMTPTransportException, MalformedSMTPReplyException { - if (from == null || from.length == 0) { + protected boolean sendMailFrom(Message message) throws MessagingException { + + // need to sort the from value out from a variety of sources. + String from = null; + + // first potential source is from the message itself, if it's an instance of SMTPMessage. + if (message instanceof SMTPMessage) { + from = ((SMTPMessage)message).getEnvelopeFrom(); + } + + // if not available from the message, check the protocol property next + if (from == null || from.length() == 0) { + // the from value can be set explicitly as a property + from = getProtocolProperty(MAIL_SMTP_FROM); + } + + + // if not there, see if we have something in the message header. + if (from == null || from.length() == 0) { + Address[] fromAddresses = message.getFrom(); + + // if we have some addresses in the header, then take the first one as our From: address + if (fromAddresses != null && fromAddresses.length > 0) { + from = ((InternetAddress)fromAddresses[0]).getAddress(); + } + // get what the InternetAddress class believes to be the local address. + else { + from = InternetAddress.getLocalAddress(session).getAddress(); + } + } + + + if (from == null || from.length() == 0) { throw new SMTPTransportException("no FROM address"); } - // TODO - what do we do w/ more than one from??? - String msg = "MAIL FROM: " + fixEmailAddress(from[0].toString()); + StringBuffer command = new StringBuffer(); - sendLine(s, msg); + // 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; + } + } - SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + // 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); + } - return !line.isError(); - } + // if we have a target, add the notification stuff to our FROM command. + if (returnNotification != null) { + command.append(" RET="); + command.append(returnNotification); + } + } - /** - * 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 - */ - protected boolean sendHelo(Socket s) throws SMTPTransportException, MalformedSMTPReplyException { - String fqdm = null; + // 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). - try { - fqdm = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - // fine, we're misconfigured - ignore + 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); + } + } } - if (fqdm == null) { - fqdm = session.getProperty(MAIL_SMTP_LOCALHOST); + 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); } - if (fqdm == null) { - throw new SMTPTransportException("Can't get local hostname. " + - " Please correctly configure JDK/DNS or set mail.smtp.localhost"); + // 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); } - sendLine(s, "HELO " + fqdm); - SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); + // and finally send the command + SMTPReply line = sendCommand(command.toString()); - return !line.isError(); + // 250 response indicates success. + return line.getCode() == COMMAND_ACCEPTED; } /** - * Get the servers welcome blob from the wire.... + * 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 boolean getWelcome(Socket s) throws SMTPTransportException, MalformedSMTPReplyException { - SMTPReply line = new SMTPReply(receiveLine(s, 5 * MIN_MILLIS)); - return !line.isError(); + 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(Socket s, String data) throws SMTPTransportException { - if (s == null) { - throw new SMTPTransportException("bonehead..."); + protected void sendLine(String data) throws MessagingException { + if (debug) { + debugOut("sending line to server >>>" + data + "<<<"); } - - if (!s.isConnected()) { + if (socket == null || !socket.isConnected()) { throw new SMTPTransportException("no connection"); } - try { - OutputStream out = s.getOutputStream(); - - out.write(data.getBytes()); - out.write(CR); - out.write(LF); - out.flush(); - - if (this.session.getDebug()) { - this.session.getDebugOut().println("sent: " + data); - } + outputStream.write(data.getBytes()); + outputStream.write(CR); + outputStream.write(LF); + outputStream.flush(); } catch (IOException e) { throw new SMTPTransportException(e); } @@ -327,15 +1531,45 @@ * 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 { + lastServerResponse = new SMTPReply(receiveLine()); + return lastServerResponse; + } + + /** + * Retrieve the last response received from the SMTP server. + * + * @return The raw response string (including the error code) returned + * from the SMTP server. + */ + public String getLastServerResponse() { + if (lastServerResponse == null) { + return ""; } + return lastServerResponse.getReply(); + } + - if (!s.isConnected()) { + + /** + * 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 SMTPTransportException("no connection"); } @@ -343,98 +1577,475 @@ try { // for now, read byte for byte, looking for a CRLF - timeout = s.getSoTimeout(); - - s.setSoTimeout(delayMillis); + timeout = socket.getSoTimeout(); - 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(); + if (debug) { + debugOut("received line from server >>>" + line + "<<<"); } + return line; - return buff.toString(); } catch (SocketException e) { throw new SMTPTransportException(e); } catch (IOException e) { throw new SMTPTransportException(e); } 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 + ">"; + } /** - * Creates and returns a connected socket + * 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 Socket getConnectedSocket() throws MessagingException { - Socket s = new Socket(); + 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(); + } - String mail_host = this.session.getProperty(MAIL_HOST); - if (mail_host == null || "".equals(mail_host)) { - mail_host = DEFAULT_MAIL_HOST; + 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"); + } } - String portString = this.session.getProperty(MAIL_SMTP_PORT); + // this worked. + return true; + } + + + /** + * Send the EHLO command to the SMTP server. + * + * @return True if the command was accepted ok, false for any errors. + * @exception SMTPTransportException + * @exception MalformedSMTPReplyException + * @exception MessagingException + */ + protected boolean sendEhlo() throws MessagingException { + sendLine("EHLO " + getLocalHost()); + + SMTPReply line = getReply(); - int port = DEFAULT_MAIL_SMTP_PORT; + // 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; + } - if (portString != null && !"".equals(portString)) { + // get a fresh extension mapping table. + serverExtensionArgs = new HashMap(); - try { - port = Integer.parseInt(portString); - } catch (NumberFormatException e) { - // ignore - we don't care, leave as default + // 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; + } - try { - if (this.session.getDebug()) { - this.session.getDebugOut().println("connecting to " + mail_host); + + /** + * Send the HELO command to the SMTP server. + * + * @exception MessagingException + */ + protected void sendHelo() throws MessagingException { + sendLine("HELO " + getLocalHost()); + + SMTPReply line = getReply(); + + // we get a 250 code back. The first line is just a greeting, and extensions are identifed on + // continuations. If this fails, then we'll try once more with HELO to establish bona fides. + if (line.getCode() != COMMAND_ACCEPTED) { + throw new MessagingException("Failure sending HELO command to SMTP server"); + } + } + + + /** + * Retrieve the local client host name. + * + * @return The string version of the local host name. + * @exception SMTPTransportException + */ + public String getLocalHost() throws MessagingException { + if (localHost == null) { + + try { + localHost = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + // fine, we're misconfigured - ignore } - s.connect(new InetSocketAddress(mail_host, port)); + if (localHost == null) { + localHost = getProtocolProperty(MAIL_SMTP_LOCALHOST); + } - if (this.session.getDebug()) { - this.session.getDebugOut().println("connected to " + mail_host); + if (localHost == null) { + localHost = getSessionProperty(MAIL_LOCALHOST); } - } catch (IOException e) { - if (this.session.getDebug()) { - this.session.getDebugOut().println("error connecting to " + mail_host); + + if (localHost == null) { + throw new SMTPTransportException("Can't get local hostname. " + + " Please correctly configure JDK/DNS or set mail.smtp.localhost"); } + } + + return localHost; + } + + + /** + * Return the current reportSuccess property. + * + * @return The current reportSuccess property. + */ + public boolean getReportSuccess() { + return reportSuccess; + } + + + /** + * Set a new value for the reportSuccess property. + * + * @param report The new setting. + */ + public void setReportSuccess(boolean report) { + reportSuccess = report; + } + + + /** + * Return the current startTLS property. + * + * @return The current startTLS property. + */ + public boolean getStartTLS() { + return reportSuccess; + } + + + /** + * Set a new value for the startTLS property. + * + * @param start The new setting. + */ + public void setStartTLS(boolean start) { + useTLS = start; + } + - throw new MessagingException("Error connecting to " + mail_host, e); + /** + * 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; + } - return s; + + /** + * Explicitly set the local host information. + * + * @param localHost The new localHost name. + */ + public void setLocalHost(String localHost) { + this.localHost = localHost; } - private String fixEmailAddress(String mail) { - if (mail.charAt(0) == '<') { - return mail; + + /** + * 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); + argument = extension.substring(delimiter + 1); + } + + // add this to the map so it can be tested later. + serverExtensionArgs.put(extensionName, argument); + + // process a few special ones that don't require extra parsing. + // AUTH and AUTH=LOGIN are handled the same + if (extensionName.equals("AUTH")) { + // if we don't have an argument on AUTH, this means LOGIN. + if (argument == null) { + serverAuthenticationMechanisms.put("LOGIN", "LOGIN"); + } + else { + // The security mechanisms are blank delimited tokens. + StringTokenizer tokenizer = new StringTokenizer(argument); + + while (tokenizer.hasMoreTokens()) { + String mechanism = tokenizer.nextToken().toUpperCase(); + serverAuthenticationMechanisms.put(mechanism, mechanism); + } + } + } + // special case for some older servers. + else if (extensionName.equals("AUTH=LOGIN")) { + serverAuthenticationMechanisms.put("LOGIN", "LOGIN"); + } + // does this support transport level security? + else if (extensionName.equals("STARTTLS")) { + // flag this for later + serverTLS = true; + } + } + + + /** + * Retrieve any argument information associated with a extension + * reported back by the server on the EHLO command. + * + * @param name The name of the target server extension. + * + * @return Any argument passed on a server extension. Returns null if + * the extension did not include an argument or the extension + * was not supported. + */ + public String extensionParameter(String name) { + if (serverExtensionArgs != null) { + return (String)serverExtensionArgs.get(name); + } + return null; + } + + /** + * Tests whether the target server supports a named extension. + * + * @param name The target extension name. + * + * @return true if the target server reported on the EHLO command that + * is supports the targer server, false if the extension was not + * supported. + */ + public boolean supportsExtension(String name) { + // this only returns null if we don't have this extension + return extensionParameter(name) != null; + } + + + /** + * Determine if the target server supports a given authentication + * mechanism. + * + * @param mechanism The mechanism name. + * + * @return true if the server EHLO response indicates it supports the + * mechanism, false otherwise. + */ + protected boolean supportsAuthentication(String mechanism) + { + return serverAuthenticationMechanisms.get(mechanism) != null; + } + + + /** + * Authenticate with the server, if necessary (or possible). + * + * @return true if we are ok to proceed, false for an authentication failures. + */ + protected boolean processAuthentication() throws MessagingException + { + // no authentication defined? + if (!isProtocolPropertyTrue(MAIL_SMTP_AUTH)) { + return true; + } + + // we need to authenticate, but we don't have userid/password information...fail this + // immediately. + if (username == null || password == null) { + return false; + } + + ClientAuthenticator authenticator = null; + + // now go through the progression of mechanisms we support, from the most secure to the + // least secure. + + if (supportsAuthentication(AUTHENTICATION_DIGESTMD5)) { + authenticator = new DigestMD5Authenticator(host, username, password, getSASLRealm()); + } + else if (supportsAuthentication(AUTHENTICATION_CRAMMD5)) { + authenticator = new CramMD5Authenticator(username, password); + } + else if (supportsAuthentication(AUTHENTICATION_LOGIN)) { + authenticator = new LoginAuthenticator(username, password); + } + else if (supportsAuthentication(AUTHENTICATION_PLAIN)) { + authenticator = new PlainAuthenticator(username, password); + } + else { + // can't find a mechanism we support in common + return false; + } + + if (debug) { + debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName()); + } + + // if the authenticator has some initial data, we compose a command containing the initial data. + if (authenticator.hasInitialResponse()) { + StringBuffer command = new StringBuffer(); + // the auth command initiates the handshaking. + command.append("AUTH "); + // and tell the server which mechanism we're using. + command.append(authenticator.getMechanismName()); + command.append(" "); + // and append the response data + command.append(new String(Base64.encode(authenticator.evaluateChallenge(null)))); + // send the command now + sendLine(command.toString()); + } + // we just send an auth command with the command type. + else { + StringBuffer command = new StringBuffer(); + // the auth command initiates the handshaking. + command.append("AUTH "); + // and tell the server which mechanism we're using. + command.append(authenticator.getMechanismName()); + // send the command now + sendLine(command.toString()); + } + + // now process the challenge sequence. We get a 235 response back when the server accepts the + // authentication, and a 334 indicates we have an additional challenge. + while (true) { + // get the next line, and if it is an error response, return now. + SMTPReply line = new SMTPReply(receiveLine()); + + // if we get a completion return, we've passed muster, so give an authentication response. + if (line.getCode() == AUTHENTICATION_COMPLETE) { + if (debug) { + debugOut("Successful SMTP authentication"); + } + return true; + } + // we have an additional challenge to process. + else if (line.getCode() == AUTHENTICATION_CHALLENGE) { + // Does the authenticator think it is finished? We can't answer an additional challenge, + // so fail this. + if (authenticator.isComplete()) { + return false; + } + + // we're passed back a challenge value, Base64 encoded. + byte[] challenge = Base64.decode(line.getMessage().getBytes()); + + // have the authenticator evaluate and send back the encoded response. + sendLine(new String(Base64.encode(authenticator.evaluateChallenge(challenge)))); + } + // completion or challenge are the only responses we know how to handle. Anything else must + // be a failure. + else { + if (debug) { + debugOut("Authentication failure " + line); + } + return false; + } } - return "<" + mail + ">"; } /** @@ -443,21 +2054,115 @@ */ [... 121 lines stripped ...]