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