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