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