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(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(String proto, 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(String proto, boolean implicit, 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(boolean implicit, 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(String protocol, 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(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(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        String reply = getReplyString().substring(4);
178        String[] parts = reply.substring(0, reply.indexOf(' ')).split ("\\.");
179        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, 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.
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(AuthenticatingSMTPClient.AUTH_METHOD method,
214                        String username, 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            byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(4).trim());
234            // get the Mac instance
235            Mac hmac_md5 = Mac.getInstance("HmacMD5");
236            hmac_md5.init(new SecretKeySpec(password.getBytes(getCharset()), "HmacMD5"));
237            // compute the result:
238            byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes(getCharset());
239            // join the byte arrays to form the reply
240            byte[] usernameBytes = username.getBytes(getCharset());
241            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))
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(byte[] a)
278    {
279        StringBuilder result = new StringBuilder(a.length*2);
280        for (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 static 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
304        /**
305         * Gets the name of the given authentication method suitable for the server.
306         * @param method The authentication method to get the name for.
307         * @return The name of the given authentication method suitable for the server.
308         */
309        public static final String getAuthName(AUTH_METHOD method)
310        {
311            if (method.equals(AUTH_METHOD.PLAIN)) {
312                return "PLAIN";
313            } else if (method.equals(AUTH_METHOD.CRAM_MD5)) {
314                return "CRAM-MD5";
315            } else if (method.equals(AUTH_METHOD.LOGIN)) {
316                return "LOGIN";
317            } else if (method.equals(AUTH_METHOD.XOAUTH)) {
318                return "XOAUTH";
319            } else {
320                return null;
321            }
322        }
323    }
324}
325
326/* kate: indent-width 4; replace-tabs on; */