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;
027
028import org.apache.commons.net.util.Base64;
029
030
031/**
032 * An SMTP Client class with authentication support (RFC4954).
033 *
034 * @see SMTPClient
035 * @since 3.0
036 */
037public class AuthenticatingSMTPClient extends SMTPSClient
038{
039    /**
040     * The default AuthenticatingSMTPClient constructor.
041     * Creates a new Authenticating SMTP Client.
042     * @throws NoSuchAlgorithmException
043     */
044    public AuthenticatingSMTPClient() throws NoSuchAlgorithmException
045    {
046        super();
047    }
048
049    /**
050     * Overloaded constructor that takes a protocol specification
051     * @param protocol The protocol to use
052     * @throws NoSuchAlgorithmException
053     */
054    public AuthenticatingSMTPClient(String protocol) throws NoSuchAlgorithmException {
055        super(protocol);
056    }
057
058    /***
059     * A convenience method to send the ESMTP EHLO command to the server,
060     * receive the reply, and return the reply code.
061     * <p>
062     * @param hostname The hostname of the sender.
063     * @return The reply code received from the server.
064     * @exception SMTPConnectionClosedException
065     *      If the SMTP server prematurely closes the connection as a result
066     *      of the client being idle or some other reason causing the server
067     *      to send SMTP reply code 421.  This exception may be caught either
068     *      as an IOException or independently as itself.
069     * @exception IOException  If an I/O error occurs while either sending the
070     *      command or receiving the server reply.
071     ***/
072    public int ehlo(String hostname) throws IOException
073    {
074        return sendCommand(SMTPCommand.EHLO, hostname);
075    }
076
077    /***
078     * Login to the ESMTP server by sending the EHLO command with the
079     * given hostname as an argument.  Before performing any mail commands,
080     * you must first login.
081     * <p>
082     * @param hostname  The hostname with which to greet the SMTP server.
083     * @return True if successfully completed, false if not.
084     * @exception SMTPConnectionClosedException
085     *      If the SMTP server prematurely closes the connection as a result
086     *      of the client being idle or some other reason causing the server
087     *      to send SMTP reply code 421.  This exception may be caught either
088     *      as an IOException or independently as itself.
089     * @exception IOException  If an I/O error occurs while either sending a
090     *      command to the server or receiving a reply from the server.
091     ***/
092    public boolean elogin(String hostname) throws IOException
093    {
094        return SMTPReply.isPositiveCompletion(ehlo(hostname));
095    }
096
097
098    /***
099     * Login to the ESMTP server by sending the EHLO command with the
100     * client hostname as an argument.  Before performing any mail commands,
101     * you must first login.
102     * <p>
103     * @return True if successfully completed, false if not.
104     * @exception SMTPConnectionClosedException
105     *      If the SMTP server prematurely closes the connection as a result
106     *      of the client being idle or some other reason causing the server
107     *      to send SMTP reply code 421.  This exception may be caught either
108     *      as an IOException or independently as itself.
109     * @exception IOException  If an I/O error occurs while either sending a
110     *      command to the server or receiving a reply from the server.
111     ***/
112    public boolean elogin() throws IOException
113    {
114        String name;
115        InetAddress host;
116
117        host = getLocalAddress();
118        name = host.getHostName();
119
120        if (name == null) {
121            return false;
122        }
123
124        return SMTPReply.isPositiveCompletion(ehlo(name));
125    }
126
127    /***
128     * Returns the integer values of the enhanced reply code of the last SMTP reply.
129     * @return The integer values of the enhanced reply code of the last SMTP reply.
130     *  First digit is in the first array element.
131     ***/
132    public int[] getEnhancedReplyCode()
133    {
134        String reply = getReplyString().substring(4);
135        String[] parts = reply.substring(0, reply.indexOf(' ')).split ("\\.");
136        int[] res = new int[parts.length];
137        for (int i = 0; i < parts.length; i++)
138        {
139            res[i] = Integer.parseInt (parts[i]);
140        }
141        return res;
142    }
143
144    /***
145     * Authenticate to the SMTP server by sending the AUTH command with the
146     * selected mechanism, using the given username and the given password.
147     * <p>
148     * @return True if successfully completed, false if not.
149     * @exception SMTPConnectionClosedException
150     *      If the SMTP server prematurely closes the connection as a result
151     *      of the client being idle or some other reason causing the server
152     *      to send SMTP reply code 421.  This exception may be caught either
153     *      as an IOException or independently as itself.
154     * @exception IOException  If an I/O error occurs while either sending a
155     *      command to the server or receiving a reply from the server.
156     * @exception NoSuchAlgorithmException If the CRAM hash algorithm
157     *      cannot be instantiated by the Java runtime system.
158     * @exception InvalidKeyException If the CRAM hash algorithm
159     *      failed to use the given password.
160     * @exception InvalidKeySpecException If the CRAM hash algorithm
161     *      failed to use the given password.
162     ***/
163    public boolean auth(AuthenticatingSMTPClient.AUTH_METHOD method,
164                        String username, String password)
165                        throws IOException, NoSuchAlgorithmException,
166                        InvalidKeyException, InvalidKeySpecException
167    {
168        if (!SMTPReply.isPositiveIntermediate(sendCommand(SMTPCommand.AUTH,
169                AUTH_METHOD.getAuthName(method)))) {
170            return false;
171        }
172
173        if (method.equals(AUTH_METHOD.PLAIN))
174        {
175            // the server sends an empty response ("334 "), so we don't have to read it.
176            return SMTPReply.isPositiveCompletion(sendCommand(
177                new String(
178                    Base64.encodeBase64(("\000" + username + "\000" + password).getBytes())
179                    )
180                ));
181        }
182        else if (method.equals(AUTH_METHOD.CRAM_MD5))
183        {
184            // get the CRAM challenge
185            byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(4).trim());
186            // get the Mac instance
187            Mac hmac_md5 = Mac.getInstance("HmacMD5");
188            hmac_md5.init(new SecretKeySpec(password.getBytes(), "HmacMD5"));
189            // compute the result:
190            byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes();
191            // join the byte arrays to form the reply
192            byte[] usernameBytes = username.getBytes();
193            byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
194            System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
195            toEncode[usernameBytes.length] = ' ';
196            System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
197            // send the reply and read the server code:
198            return SMTPReply.isPositiveCompletion(sendCommand(
199                new String(Base64.encodeBase64(toEncode))));
200        }
201        else if (method.equals(AUTH_METHOD.LOGIN))
202        {
203            // the server sends fixed responses (base64("Username") and
204            // base64("Password")), so we don't have to read them.
205            if (!SMTPReply.isPositiveIntermediate(sendCommand(
206                new String(Base64.encodeBase64(username.getBytes()))))) {
207                return false;
208            }
209            return SMTPReply.isPositiveCompletion(sendCommand(
210                new String(Base64.encodeBase64(password.getBytes()))));
211        } else {
212            return false; // safety check
213        }
214    }
215
216    /**
217     * Converts the given byte array to a String containing the hex values of the bytes.
218     * For example, the byte 'A' will be converted to '41', because this is the ASCII code
219     * (and the byte value) of the capital letter 'A'.
220     * @param a The byte array to convert.
221     * @return The resulting String of hex codes.
222     */
223    private String _convertToHexString(byte[] a)
224    {
225        StringBuilder result = new StringBuilder(a.length*2);
226        for (int i = 0; i < a.length; i++)
227        {
228            if ( (a[i] & 0x0FF) <= 15 ) {
229                result.append("0");
230            }
231            result.append(Integer.toHexString(a[i] & 0x0FF));
232        }
233        return result.toString();
234    }
235
236    /**
237     * The enumeration of currently-supported authentication methods.
238     */
239    public static enum AUTH_METHOD
240    {
241        /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
242        PLAIN,
243        /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
244        CRAM_MD5,
245        /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
246        LOGIN;
247
248        /**
249         * Gets the name of the given authentication method suitable for the server.
250         * @param method The authentication method to get the name for.
251         * @return The name of the given authentication method suitable for the server.
252         */
253        public static final String getAuthName(AUTH_METHOD method)
254        {
255            if (method.equals(AUTH_METHOD.PLAIN)) {
256                return "PLAIN";
257            } else if (method.equals(AUTH_METHOD.CRAM_MD5)) {
258                return "CRAM-MD5";
259            } else if (method.equals(AUTH_METHOD.LOGIN)) {
260                return "LOGIN";
261            } else {
262                return null;
263            }
264        }
265    }
266}
267
268/* kate: indent-width 4; replace-tabs on; */