    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
    018    package org.apache.commons.net.smtp;
    019
    020    import java.io.IOException;
    021    import java.net.InetAddress;
    022    import java.security.InvalidKeyException;
    023    import java.security.NoSuchAlgorithmException;
    024    import java.security.spec.InvalidKeySpecException;
    025    import javax.crypto.Mac;
    026    import javax.crypto.spec.SecretKeySpec;
    027
    028    import 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     */
    037    public 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         *
    148         * @param method the method to use, one of the {@link AuthenticatingSMTPClient.AUTH_METHOD} enum values
    149         * @param username the user name.
    150         *        If the method is XOAUTH, then this is used as the plain text oauth protocol parameter string
    151         *        which is Base64-encoded for transmission.
    152         * @param password the password for the username.
    153         *        Ignored for XOAUTH.
    154         *
    155         * @return True if successfully completed, false if not.
    156         * @exception SMTPConnectionClosedException
    157         *      If the SMTP server prematurely closes the connection as a result
    158         *      of the client being idle or some other reason causing the server
    159         *      to send SMTP reply code 421.  This exception may be caught either
    160         *      as an IOException or independently as itself.
    161         * @exception IOException  If an I/O error occurs while either sending a
    162         *      command to the server or receiving a reply from the server.
    163         * @exception NoSuchAlgorithmException If the CRAM hash algorithm
    164         *      cannot be instantiated by the Java runtime system.
    165         * @exception InvalidKeyException If the CRAM hash algorithm
    166         *      failed to use the given password.
    167         * @exception InvalidKeySpecException If the CRAM hash algorithm
    168         *      failed to use the given password.
    169         ***/
    170        public boolean auth(AuthenticatingSMTPClient.AUTH_METHOD method,
    171                            String username, String password)
    172                            throws IOException, NoSuchAlgorithmException,
    173                            InvalidKeyException, InvalidKeySpecException
    174        {
    175            if (!SMTPReply.isPositiveIntermediate(sendCommand(SMTPCommand.AUTH,
    176                    AUTH_METHOD.getAuthName(method)))) {
    177                return false;
    178            }
    179
    180            if (method.equals(AUTH_METHOD.PLAIN))
    181            {
    182                // the server sends an empty response ("334 "), so we don't have to read it.
    183                return SMTPReply.isPositiveCompletion(sendCommand(
    184                        Base64.encodeBase64StringUnChunked(("\000" + username + "\000" + password).getBytes())
    185                    ));
    186            }
    187            else if (method.equals(AUTH_METHOD.CRAM_MD5))
    188            {
    189                // get the CRAM challenge
    190                byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(4).trim());
    191                // get the Mac instance
    192                Mac hmac_md5 = Mac.getInstance("HmacMD5");
    193                hmac_md5.init(new SecretKeySpec(password.getBytes(), "HmacMD5"));
    194                // compute the result:
    195                byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes();
    196                // join the byte arrays to form the reply
    197                byte[] usernameBytes = username.getBytes();
    198                byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
    199                System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
    200                toEncode[usernameBytes.length] = ' ';
    201                System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
    202                // send the reply and read the server code:
    203                return SMTPReply.isPositiveCompletion(sendCommand(
    204                    Base64.encodeBase64StringUnChunked(toEncode)));
    205            }
    206            else if (method.equals(AUTH_METHOD.LOGIN))
    207            {
    208                // the server sends fixed responses (base64("Username") and
    209                // base64("Password")), so we don't have to read them.
    210                if (!SMTPReply.isPositiveIntermediate(sendCommand(
    211                    Base64.encodeBase64StringUnChunked(username.getBytes())))) {
    212                    return false;
    213                }
    214                return SMTPReply.isPositiveCompletion(sendCommand(
    215                    Base64.encodeBase64StringUnChunked(password.getBytes())));
    216            }
    217            else if (method.equals(AUTH_METHOD.XOAUTH))
    218            {
    219                return SMTPReply.isPositiveIntermediate(sendCommand(
    220                        Base64.encodeBase64StringUnChunked(username.getBytes())
    221                ));
    222            } else {
    223                return false; // safety check
    224            }
    225        }
    226
    227        /**
    228         * Converts the given byte array to a String containing the hex values of the bytes.
    229         * For example, the byte 'A' will be converted to '41', because this is the ASCII code
    230         * (and the byte value) of the capital letter 'A'.
    231         * @param a The byte array to convert.
    232         * @return The resulting String of hex codes.
    233         */
    234        private String _convertToHexString(byte[] a)
    235        {
    236            StringBuilder result = new StringBuilder(a.length*2);
    237            for (byte element : a)
    238            {
    239                if ( (element & 0x0FF) <= 15 ) {
    240                    result.append("0");
    241                }
    242                result.append(Integer.toHexString(element & 0x0FF));
    243            }
    244            return result.toString();
    245        }
    246
    247        /**
    248         * The enumeration of currently-supported authentication methods.
    249         */
    250        public static enum AUTH_METHOD
    251        {
    252            /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
    253            PLAIN,
    254            /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
    255            CRAM_MD5,
    256            /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
    257            LOGIN,
    258            /** XOAuth method which accepts a signed and base64ed OAuth URL. */
    259            XOAUTH;
    260
    261            /**
    262             * Gets the name of the given authentication method suitable for the server.
    263             * @param method The authentication method to get the name for.
    264             * @return The name of the given authentication method suitable for the server.
    265             */
    266            public static final String getAuthName(AUTH_METHOD method)
    267            {
    268                if (method.equals(AUTH_METHOD.PLAIN)) {
    269                    return "PLAIN";
    270                } else if (method.equals(AUTH_METHOD.CRAM_MD5)) {
    271                    return "CRAM-MD5";
    272                } else if (method.equals(AUTH_METHOD.LOGIN)) {
    273                    return "LOGIN";
    274                } else if (method.equals(AUTH_METHOD.XOAUTH)) {
    275                    return "XOAUTH";
    276                } else {
    277                    return null;
    278                }
    279            }
    280        }
    281    }
    282
    283    /* kate: indent-width 4; replace-tabs on; */