001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.smtp;
019
020import java.io.IOException;
021import java.net.InetAddress;
022import java.security.InvalidKeyException;
023import java.security.NoSuchAlgorithmException;
024import java.security.spec.InvalidKeySpecException;
025import javax.crypto.Mac;
026import javax.crypto.spec.SecretKeySpec;
027import javax.net.ssl.SSLContext;
028
029import org.apache.commons.net.util.Base64;
030
031
032/**
033 * An SMTP Client class with authentication support (RFC4954).
034 *
035 * @see SMTPClient
036 * @since 3.0
037 */
038public class AuthenticatingSMTPClient extends SMTPSClient
039{
040    /**
041     * The default AuthenticatingSMTPClient constructor.
042     * Creates a new Authenticating SMTP Client.
043     * @throws NoSuchAlgorithmException
044     */
045    public AuthenticatingSMTPClient() throws NoSuchAlgorithmException
046    {
047        super();
048    }
049
050    /**
051     * Overloaded constructor that takes a protocol specification
052     * @param protocol The protocol to use
053     * @throws NoSuchAlgorithmException
054     */
055    public AuthenticatingSMTPClient(String protocol) throws NoSuchAlgorithmException {
056        super(protocol);
057    }
058
059    /**
060     * Overloaded constructor that takes a protocol specification and the implicit argument
061     * @param proto the protocol.
062     * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
063     * @since 3.3
064     */
065    public AuthenticatingSMTPClient(String proto, boolean implicit)
066    {
067      super(proto, implicit);
068    }
069
070    /**
071     * Overloaded constructor that takes the protocol specification, the implicit argument and encoding
072     * @param proto the protocol.
073     * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
074     * @param encoding the encoding
075     * @since 3.3
076     */
077    public AuthenticatingSMTPClient(String proto, boolean implicit, String encoding)
078    {
079      super(proto, implicit, encoding);
080    }
081
082    /**
083     * Overloaded constructor that takes the implicit argument, and using {@link #DEFAULT_PROTOCOL} i.e. TLS
084     * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
085     * @param ctx A pre-configured SSL Context.
086     * @since 3.3
087     */
088    public AuthenticatingSMTPClient(boolean implicit, SSLContext ctx)
089    {
090      super(implicit, ctx);
091    }
092
093    /**
094     * Overloaded constructor that takes a protocol specification and encoding
095     * @param protocol The protocol to use
096     * @param encoding The encoding to use
097     * @throws NoSuchAlgorithmException
098     * @since 3.3
099     */
100    public AuthenticatingSMTPClient(String protocol, String encoding) throws NoSuchAlgorithmException {
101        super(protocol, false, encoding);
102    }
103
104    /***
105     * A convenience method to send the ESMTP EHLO command to the server,
106     * receive the reply, and return the reply code.
107     * <p>
108     * @param hostname The hostname of the sender.
109     * @return The reply code received from the server.
110     * @exception SMTPConnectionClosedException
111     *      If the SMTP server prematurely closes the connection as a result
112     *      of the client being idle or some other reason causing the server
113     *      to send SMTP reply code 421.  This exception may be caught either
114     *      as an IOException or independently as itself.
115     * @exception IOException  If an I/O error occurs while either sending the
116     *      command or receiving the server reply.
117     ***/
118    public int ehlo(String hostname) throws IOException
119    {
120        return sendCommand(SMTPCommand.EHLO, hostname);
121    }
122
123    /***
124     * Login to the ESMTP server by sending the EHLO command with the
125     * given hostname as an argument.  Before performing any mail commands,
126     * you must first login.
127     * <p>
128     * @param hostname  The hostname with which to greet the SMTP server.
129     * @return True if successfully completed, false if not.
130     * @exception SMTPConnectionClosedException
131     *      If the SMTP server prematurely closes the connection as a result
132     *      of the client being idle or some other reason causing the server
133     *      to send SMTP reply code 421.  This exception may be caught either
134     *      as an IOException or independently as itself.
135     * @exception IOException  If an I/O error occurs while either sending a
136     *      command to the server or receiving a reply from the server.
137     ***/
138    public boolean elogin(String hostname) throws IOException
139    {
140        return SMTPReply.isPositiveCompletion(ehlo(hostname));
141    }
142
143
144    /***
145     * Login to the ESMTP server by sending the EHLO command with the
146     * client hostname as an argument.  Before performing any mail commands,
147     * you must first login.
148     * <p>
149     * @return True if successfully completed, false if not.
150     * @exception SMTPConnectionClosedException
151     *      If the SMTP server prematurely closes the connection as a result
152     *      of the client being idle or some other reason causing the server
153     *      to send SMTP reply code 421.  This exception may be caught either
154     *      as an IOException or independently as itself.
155     * @exception IOException  If an I/O error occurs while either sending a
156     *      command to the server or receiving a reply from the server.
157     ***/
158    public boolean elogin() throws IOException
159    {
160        String name;
161        InetAddress host;
162
163        host = getLocalAddress();
164        name = host.getHostName();
165
166        if (name == null) {
167            return false;
168        }
169
170        return SMTPReply.isPositiveCompletion(ehlo(name));
171    }
172
173    /***
174     * Returns the integer values of the enhanced reply code of the last SMTP reply.
175     * @return The integer values of the enhanced reply code of the last SMTP reply.
176     *  First digit is in the first array element.
177     ***/
178    public int[] getEnhancedReplyCode()
179    {
180        String reply = getReplyString().substring(4);
181        String[] parts = reply.substring(0, reply.indexOf(' ')).split ("\\.");
182        int[] res = new int[parts.length];
183        for (int i = 0; i < parts.length; i++)
184        {
185            res[i] = Integer.parseInt (parts[i]);
186        }
187        return res;
188    }
189
190    /***
191     * Authenticate to the SMTP server by sending the AUTH command with the
192     * selected mechanism, using the given username and the given password.
193     *
194     * @param method the method to use, one of the {@link AuthenticatingSMTPClient.AUTH_METHOD} enum values
195     * @param username the user name.
196     *        If the method is XOAUTH, then this is used as the plain text oauth protocol parameter string
197     *        which is Base64-encoded for transmission.
198     * @param password the password for the username.
199     *        Ignored for XOAUTH.
200     *
201     * @return True if successfully completed, false if not.
202     * @exception SMTPConnectionClosedException
203     *      If the SMTP server prematurely closes the connection as a result
204     *      of the client being idle or some other reason causing the server
205     *      to send SMTP reply code 421.  This exception may be caught either
206     *      as an IOException or independently as itself.
207     * @exception IOException  If an I/O error occurs while either sending a
208     *      command to the server or receiving a reply from the server.
209     * @exception NoSuchAlgorithmException If the CRAM hash algorithm
210     *      cannot be instantiated by the Java runtime system.
211     * @exception InvalidKeyException If the CRAM hash algorithm
212     *      failed to use the given password.
213     * @exception InvalidKeySpecException If the CRAM hash algorithm
214     *      failed to use the given password.
215     ***/
216    public boolean auth(AuthenticatingSMTPClient.AUTH_METHOD method,
217                        String username, String password)
218                        throws IOException, NoSuchAlgorithmException,
219                        InvalidKeyException, InvalidKeySpecException
220    {
221        if (!SMTPReply.isPositiveIntermediate(sendCommand(SMTPCommand.AUTH,
222                AUTH_METHOD.getAuthName(method)))) {
223            return false;
224        }
225
226        if (method.equals(AUTH_METHOD.PLAIN))
227        {
228            // the server sends an empty response ("334 "), so we don't have to read it.
229            return SMTPReply.isPositiveCompletion(sendCommand(
230                    Base64.encodeBase64StringUnChunked(("\000" + username + "\000" + password).getBytes(getCharsetName())) // Java 1.6 can use getCharset()
231                ));
232        }
233        else if (method.equals(AUTH_METHOD.CRAM_MD5))
234        {
235            // get the CRAM challenge
236            byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(4).trim());
237            // get the Mac instance
238            Mac hmac_md5 = Mac.getInstance("HmacMD5");
239            hmac_md5.init(new SecretKeySpec(password.getBytes(getCharsetName()), "HmacMD5")); // Java 1.6 can use getCharset()
240            // compute the result:
241            byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes(getCharsetName()); // Java 1.6 can use getCharset()
242            // join the byte arrays to form the reply
243            byte[] usernameBytes = username.getBytes(getCharsetName()); // Java 1.6 can use getCharset()
244            byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
245            System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
246            toEncode[usernameBytes.length] = ' ';
247            System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
248            // send the reply and read the server code:
249            return SMTPReply.isPositiveCompletion(sendCommand(
250                Base64.encodeBase64StringUnChunked(toEncode)));
251        }
252        else if (method.equals(AUTH_METHOD.LOGIN))
253        {
254            // the server sends fixed responses (base64("Username") and
255            // base64("Password")), so we don't have to read them.
256            if (!SMTPReply.isPositiveIntermediate(sendCommand(
257                Base64.encodeBase64StringUnChunked(username.getBytes(getCharsetName()))))) { // Java 1.6 can use getCharset()
258                return false;
259            }
260            return SMTPReply.isPositiveCompletion(sendCommand(
261                Base64.encodeBase64StringUnChunked(password.getBytes(getCharsetName())))); // Java 1.6 can use getCharset()
262        }
263        else if (method.equals(AUTH_METHOD.XOAUTH))
264        {
265            return SMTPReply.isPositiveIntermediate(sendCommand(
266                    Base64.encodeBase64StringUnChunked(username.getBytes(getCharsetName())) // Java 1.6 can use getCharset()
267            ));
268        } else {
269            return false; // safety check
270        }
271    }
272
273    /**
274     * Converts the given byte array to a String containing the hex values of the bytes.
275     * For example, the byte 'A' will be converted to '41', because this is the ASCII code
276     * (and the byte value) of the capital letter 'A'.
277     * @param a The byte array to convert.
278     * @return The resulting String of hex codes.
279     */
280    private String _convertToHexString(byte[] a)
281    {
282        StringBuilder result = new StringBuilder(a.length*2);
283        for (byte element : a)
284        {
285            if ( (element & 0x0FF) <= 15 ) {
286                result.append("0");
287            }
288            result.append(Integer.toHexString(element & 0x0FF));
289        }
290        return result.toString();
291    }
292
293    /**
294     * The enumeration of currently-supported authentication methods.
295     */
296    public static enum AUTH_METHOD
297    {
298        /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
299        PLAIN,
300        /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
301        CRAM_MD5,
302        /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
303        LOGIN,
304        /** XOAuth method which accepts a signed and base64ed OAuth URL. */
305        XOAUTH;
306
307        /**
308         * Gets the name of the given authentication method suitable for the server.
309         * @param method The authentication method to get the name for.
310         * @return The name of the given authentication method suitable for the server.
311         */
312        public static final String getAuthName(AUTH_METHOD method)
313        {
314            if (method.equals(AUTH_METHOD.PLAIN)) {
315                return "PLAIN";
316            } else if (method.equals(AUTH_METHOD.CRAM_MD5)) {
317                return "CRAM-MD5";
318            } else if (method.equals(AUTH_METHOD.LOGIN)) {
319                return "LOGIN";
320            } else if (method.equals(AUTH_METHOD.XOAUTH)) {
321                return "XOAUTH";
322            } else {
323                return null;
324            }
325        }
326    }
327}
328
329/* kate: indent-width 4; replace-tabs on; */