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