    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; */