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