diff -ruN commons-net-2.0-src/src/main/java/org/apache/commons/net/SocketClient.java commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/SocketClient.java --- commons-net-2.0-src/src/main/java/org/apache/commons/net/SocketClient.java 2008-10-19 18:47:24.000000000 +0200 +++ commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/SocketClient.java 2009-08-20 23:16:26.000000000 +0200 @@ -28,6 +28,7 @@ import javax.net.ServerSocketFactory; import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; /** @@ -578,7 +579,22 @@ public int getConnectTimeout() { return connectTimeout; } - + + /** + * Start TLS operation on an already connected socket. + * @return + */ + public void startTLS() + throws IOException + { + SSLSocketFactory factory = + (SSLSocketFactory)SSLSocketFactory.getDefault(); + InetSocketAddress isa = + (InetSocketAddress)this._socket_.getRemoteSocketAddress(); + Socket s = factory.createSocket(this._socket_, isa.getHostName(), + isa.getPort(), true); + this._socket_ = s; + } } diff -ruN commons-net-2.0-src/src/main/java/org/apache/commons/net/smtp/SMTP.java commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/smtp/SMTP.java --- commons-net-2.0-src/src/main/java/org/apache/commons/net/smtp/SMTP.java 2008-10-19 18:47:15.000000000 +0200 +++ commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/smtp/SMTP.java 2009-08-20 23:35:10.000000000 +0200 @@ -15,6 +15,46 @@ * limitations under the License. */ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can obtain + * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html + * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt. + * Sun designates this particular file as subject to the "Classpath" exception + * as provided by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the License + * Header, with the fields enclosed by brackets [] replaced by your own + * identifying information: "Portions Copyrighted [year] + * [name of copyright owner]" + * + * Contributor(s): + * + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ + +/* + * Portions Copyrighted 2009 Nilvec, http://nilvec.com. + */ + package org.apache.commons.net.smtp; import java.io.BufferedReader; @@ -24,6 +64,11 @@ import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; import org.apache.commons.net.MalformedServerReplyException; import org.apache.commons.net.ProtocolCommandListener; @@ -113,6 +158,16 @@ protected ProtocolCommandSupport _commandSupport_; /*** + * Extensions supported by the server. + */ + protected Hashtable _extensions_; + + /*** + * Is TLS already started. + */ + protected boolean _isTLSStarted_ = false; + + /*** * The default SMTP constructor. Sets the default port to * DEFAULT_PORT and initializes internal data structures * for saving SMTP reply information. @@ -125,6 +180,7 @@ _newReplyString = false; _replyString = null; _commandSupport_ = new ProtocolCommandSupport(this); + _extensions_ = new Hashtable(); } /** @@ -137,6 +193,64 @@ this.encoding = encoding; } + public boolean isExtensionSupported(String ext) { + return _extensions_ != null && + _extensions_.get(ext.toUpperCase(Locale.ENGLISH)) != null; + } + + public String getExtensionParameter(String ext) { + return _extensions_ == null ? null : + (String)_extensions_.get(ext.toUpperCase(Locale.ENGLISH)); + } + + public boolean isAuthenticationSupported(String auth) { + if (_extensions_ == null) + return false; + + String a = (String)_extensions_.get("AUTH"); + if (a == null) + return false; + + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equalsIgnoreCase(auth)) + return true; + } + + // hack for buggy servers that advertise capability incorrectly + if (auth.equalsIgnoreCase("LOGIN") && + isExtensionSupported("AUTH=LOGIN")) { + return true; + } + + return false; + } + + protected void _parseExtensions_(List lines) { + Iterator it = lines.iterator(); + boolean first = true; + while (it.hasNext()) { + String line = it.next(); + if (first) { // skip first line which is the greeting + first = false; + continue; + } + + if (line.length() < 5) + continue; // shouldn't happen + + line = line.substring(4); // skip response code + int i = line.indexOf(' '); + String arg = ""; + if (i > 0) { + arg = line.substring(i + 1); + line = line.substring(0, i); + } + _extensions_.put(line.toUpperCase(Locale.ENGLISH), arg); + } + } + private int __sendCommand(String command, String args, boolean includeSpace) throws IOException { @@ -495,6 +609,61 @@ return sendCommand(SMTPCommand.HELO, hostname); } + /*** + * A convenience method to send the SMTP EHLO command to the server, + * receive the reply, and return the reply code. + *

+ * @param hostname The hostname of the sender. + * @return The reply code received from the server. + * @exception SMTPConnectionClosedException + * If the SMTP server prematurely closes the connection as a result + * of the client being idle or some other reason causing the server + * to send SMTP reply code 421. This exception may be caught either + * as an IOException or independently as itself. + * @exception IOException If an I/O error occurs while either sending the + * command or receiving the server reply. + ***/ + public int ehlo(String hostname) throws IOException + { + int ret = sendCommand(SMTPCommand.EHLO, hostname); + _parseExtensions_(_replyLines); + return ret; + } + + protected void _startTLSAction_() throws IOException { + super._connectAction_(); + _reader = + new BufferedReader(new InputStreamReader(_input_, + encoding)); + _writer = + new BufferedWriter(new OutputStreamWriter(_output_, + encoding)); + } + + /*** + * A convenience method to send the SMTP STARTTLS command to the server, + * receive the reply, and return the reply code. + *

+ * @return The reply code received from the server. + * @exception SMTPConnectionClosedException + * If the SMTP server prematurely closes the connection as a result + * of the client being idle or some other reason causing the server + * to send SMTP reply code 421. This exception may be caught either + * as an IOException or independently as itself. + * @exception IOException If an I/O error occurs while either sending the + * command or receiving the server reply. + ***/ + public int starttls() throws IOException { + int ret = sendCommand(SMTPCommand.STARTTLS); + if (!SMTPReply.isPositiveCompletion(ret)) + return ret; + + startTLS(); + _startTLSAction_(); + _isTLSStarted_ = true; + + return ret; + } /*** * A convenience method to send the SMTP MAIL command to the server, diff -ruN commons-net-2.0-src/src/main/java/org/apache/commons/net/smtp/SMTPClient.java commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/smtp/SMTPClient.java --- commons-net-2.0-src/src/main/java/org/apache/commons/net/smtp/SMTPClient.java 2008-10-19 18:47:15.000000000 +0200 +++ commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/smtp/SMTPClient.java 2009-08-20 23:35:10.000000000 +0200 @@ -15,12 +15,57 @@ * limitations under the License. */ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can obtain + * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html + * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt. + * Sun designates this particular file as subject to the "Classpath" exception + * as provided by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the License + * Header, with the fields enclosed by brackets [] replaced by your own + * identifying information: "Portions Copyrighted [year] + * [name of copyright owner]" + * + * Contributor(s): + * + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ + +/* + * Portions Copyrighted 2009 Nilvec, http://nilvec.com. + */ + package org.apache.commons.net.smtp; import java.io.IOException; import java.io.Writer; import java.net.InetAddress; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.net.io.DotTerminatedMessageWriter; /*** @@ -126,10 +171,20 @@ public class SMTPClient extends SMTP { + private boolean useEhlo = true; + private boolean useAuth = false; + private boolean useStartTLS = true; + private boolean requireStartTLS = false; + + private String authMechanisms; + private Map authenticators; + /** * Default SMTPClient constructor. Creates a new SMTPClient instance. */ - public SMTPClient() { } + public SMTPClient() { + createAuthenticators(); + } /** * Overloaded constructor that takes an encoding specification @@ -138,8 +193,22 @@ */ public SMTPClient(String encoding) { super(encoding); + createAuthenticators(); } + private void createAuthenticators() { + Authenticator[] a = new Authenticator[] { + new LoginAuthenticator(), + new PlainAuthenticator() + }; + authenticators = new HashMap(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < a.length; i++) { + authenticators.put(a[i].getMechanism(), a[i]); + sb.append(a[i].getMechanism()).append(' '); + } + authMechanisms = sb.toString(); + } /*** * At least one SMTPClient method ({@link #sendMessageData sendMessageData }) @@ -180,7 +249,7 @@ /*** - * Login to the SMTP server by sending the HELO command with the + * Login to the SMTP server by sending the HELO/EHLO command with the * given hostname as an argument. Before performing any mail commands, * you must first login. *

@@ -196,7 +265,81 @@ ***/ public boolean login(String hostname) throws IOException { - return SMTPReply.isPositiveCompletion(helo(hostname)); + return login(hostname, null, null); + } + + + /*** + * Login to the SMTP server by sending the HELO/EHLO command with the + * given hostname as an argument. Before performing any mail commands, + * you must first login. + *

+ * @param hostname The hostname with which to greet the SMTP server. + * @return True if successfully completed, false if not. + * @exception SMTPConnectionClosedException + * If the SMTP server prematurely closes the connection as a result + * of the client being idle or some other reason causing the server + * to send SMTP reply code 421. This exception may be caught either + * as an IOException or independently as itself. + * @exception IOException If an I/O error occurs while either sending a + * command to the server or receiving a reply from the server. + ***/ + public boolean login(String hostname, String username, String password) + throws IOException + { + if (useAuth && (username == null || password == null)) + return false; + + boolean success = false; + + // HELO/EHLO + if (useEhlo) + success = SMTPReply.isPositiveCompletion(ehlo(hostname)); + if (!success) + success = SMTPReply.isPositiveCompletion(helo(hostname)); + if (!success) + return false; + + // STARTTLS + if (useStartTLS || requireStartTLS) { + if (isExtensionSupported("STARTTLS")) { + starttls(); + /* + * Have to issue another EHLO to update list of _extensions_ + * supported, especially authentication mechanisms. + * Don't know if this could ever fail, but we ignore failure. + */ + ehlo(hostname); + } else if (requireStartTLS) { + return false; + } + } + + if ((useAuth || (username != null && password != null)) && + (isExtensionSupported("AUTH") || + isExtensionSupported("AUTH=LOGIN"))) { + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * use it. + */ + StringTokenizer st = new StringTokenizer(authMechanisms); + while (st.hasMoreTokens()) { + String m = st.nextToken().toUpperCase(Locale.ENGLISH); + if (!isAuthenticationSupported(m)) + continue; + Authenticator a = (Authenticator)authenticators.get(m); + if (a == null) + continue; + // only first supported mechanism is used + return a.authenticate(hostname, username, password); + } + // if authentication fails, close connection and return false + } + + return true; } @@ -604,4 +747,178 @@ return SMTPReply.isPositiveCompletion(noop()); } + public boolean getUseEhlo() { + return useEhlo; + } + + public void setUseEhlo(boolean useEhlo) { + this.useEhlo = useEhlo; + } + + public boolean getUseAuth() { + return useAuth; + } + + public void setUseAuth(boolean useAuth) { + this.useAuth = useAuth; + } + + public boolean getUseStartTLS() { + return useStartTLS; + } + + public void setUseStartTLS(boolean useStartTLS) { + this.useStartTLS = useStartTLS; + } + + public boolean getRequireStartTLS() { + return requireStartTLS; + } + + public void setRequireStartTLS(boolean requireStartTLS) { + this.requireStartTLS = requireStartTLS; + } + + public String getAuthMechanisms() { + return authMechanisms; + } + + public void setAuthMechanisms(String authMechanisms) { + this.authMechanisms = authMechanisms; + } + + /** + * Abstract base class for SMTP authentication mechanism implementations. + */ + private abstract class Authenticator { + + protected int resp; // the response code, used by subclasses + private String mech; // the mechanism name, set in the constructor + + Authenticator(String mech) { + this.mech = mech.toUpperCase(Locale.ENGLISH); + } + + String getMechanism() { + return mech; + } + + /** + * Start the authentication handshake by issuing the AUTH command. + * Delegate to the doAuth method to do the mechanism-specific + * part of the handshake. + */ + boolean authenticate(String host, String user, String passwd) + throws IOException { + // XXX - could use "initial response" capability + resp = sendCommand("AUTH " + mech); + + /* + * A 530 response indicates that the server wants us to + * issue a STARTTLS command first. Do that and try again. + */ + if (resp == 530) { + starttls(); + resp = sendCommand("AUTH " + mech); + } + try { + if (resp == 334) + doAuth(host, user, passwd); + } catch (IOException ex) { // should never happen, ignore + } finally { + if (resp != 235) { + return false; + } + } + return true; + } + + abstract void doAuth(String host, String user, String passwd) + throws IOException, IOException; + } + + /** + * Perform the authentication handshake for LOGIN authentication. + */ + private class LoginAuthenticator extends Authenticator { + LoginAuthenticator() { + super("LOGIN"); + } + + void doAuth(String host, String user, String passwd) + throws IOException, IOException { + // send username + resp = sendCommand( + new String(Base64.encodeBase64(user.getBytes()))); + if (resp == 334) { + // send passwd + resp = sendCommand( + new String(Base64.encodeBase64(passwd.getBytes()))); + } + } + } + + /** + * Perform the authentication handshake for PLAIN authentication. + */ + private class PlainAuthenticator extends Authenticator { + PlainAuthenticator() { + super("PLAIN"); + } + + void doAuth(String host, String user, String passwd) + throws IOException, IOException { + // send "userpasswd" + // XXX - we don't send an authorization identity + String str = ""; + byte[] n = { 0 }; + str += Base64.encodeBase64(n); + str += Base64.encodeBase64(user.getBytes()); + str += Base64.encodeBase64(n); + str += Base64.encodeBase64(passwd.getBytes()); + + // send username + resp = sendCommand(str); + } + } + + /** + * Perform the authentication handshake for DIGEST-MD5 authentication. + */ + /*private class DigestMD5Authenticator extends Authenticator { + private DigestMD5 md5support; // only create if needed + + DigestMD5Authenticator() { + super("DIGEST-MD5"); + } + + private synchronized DigestMD5 getMD5() { + if (md5support == null) + md5support = new DigestMD5(debug ? out : null); + return md5support; + } + + void doAuth(String host, String user, String passwd) + throws IOException, IOException { + DigestMD5 md5 = getMD5(); + if (md5 == null) { + resp = -1; + return; // XXX - should never happen + } + + byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), + getLastServerResponse()); + resp = simpleCommand(b); + if (resp == 334) { // client authenticated by server + if (!md5.authServer(getLastServerResponse())) { + // server NOT authenticated by client !!! + resp = -1; + } else { + // send null response + resp = simpleCommand(new byte[0]); + } + } + } + }*/ + } diff -ruN commons-net-2.0-src/src/main/java/org/apache/commons/net/smtp/SMTPCommand.java commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/smtp/SMTPCommand.java --- commons-net-2.0-src/src/main/java/org/apache/commons/net/smtp/SMTPCommand.java 2008-10-19 18:47:15.000000000 +0200 +++ commons-net-2.0-src_esmtp+auth_patched/src/main/java/org/apache/commons/net/smtp/SMTPCommand.java 2009-08-20 23:35:10.000000000 +0200 @@ -47,6 +47,8 @@ public static final int NOOP = 11; public static final int TURN = 12; public static final int QUIT = 13; + public static final int EHLO = 14; + public static final int STARTTLS = 15; public static final int HELLO = HELO; public static final int LOGIN = HELO; @@ -71,7 +73,8 @@ static final String[] _commands = { "HELO", "MAIL FROM:", "RCPT TO:", "DATA", "SEND FROM:", "SOML FROM:", - "SAML FROM:", "RSET", "VRFY", "EXPN", "HELP", "NOOP", "TURN", "QUIT" + "SAML FROM:", "RSET", "VRFY", "EXPN", "HELP", "NOOP", "TURN", "QUIT", + "EHLO", "STARTTLS" };