View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.smtp;
19  
20  import java.io.IOException;
21  import java.net.InetAddress;
22  import java.security.InvalidKeyException;
23  import java.security.NoSuchAlgorithmException;
24  import java.security.spec.InvalidKeySpecException;
25  import javax.crypto.Mac;
26  import javax.crypto.spec.SecretKeySpec;
27  import javax.net.ssl.SSLContext;
28  
29  import org.apache.commons.net.util.Base64;
30  
31  
32  /**
33   * An SMTP Client class with authentication support (RFC4954).
34   *
35   * @see SMTPClient
36   * @since 3.0
37   */
38  public class AuthenticatingSMTPClient extends SMTPSClient
39  {
40      /**
41       * The default AuthenticatingSMTPClient constructor.
42       * Creates a new Authenticating SMTP Client.
43       * @throws NoSuchAlgorithmException
44       */
45      public AuthenticatingSMTPClient() throws NoSuchAlgorithmException
46      {
47          super();
48      }
49  
50      /**
51       * Overloaded constructor that takes a protocol specification
52       * @param protocol The protocol to use
53       * @throws NoSuchAlgorithmException
54       */
55      public AuthenticatingSMTPClient(String protocol) throws NoSuchAlgorithmException {
56          super(protocol);
57      }
58  
59      /**
60       * Overloaded constructor that takes a protocol specification and the implicit argument
61       * @param proto the protocol.
62       * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
63       * @since 3.3
64       */
65      public AuthenticatingSMTPClient(String proto, boolean implicit)
66      {
67        super(proto, implicit);
68      }
69  
70      /**
71       * Overloaded constructor that takes the protocol specification, the implicit argument and encoding
72       * @param proto the protocol.
73       * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
74       * @param encoding the encoding
75       * @since 3.3
76       */
77      public AuthenticatingSMTPClient(String proto, boolean implicit, String encoding)
78      {
79        super(proto, implicit, encoding);
80      }
81  
82      /**
83       * Overloaded constructor that takes the implicit argument, and using {@link #DEFAULT_PROTOCOL} i.e. TLS
84       * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
85       * @param ctx A pre-configured SSL Context.
86       * @since 3.3
87       */
88      public AuthenticatingSMTPClient(boolean implicit, SSLContext ctx)
89      {
90        super(implicit, ctx);
91      }
92  
93      /**
94       * Overloaded constructor that takes a protocol specification and encoding
95       * @param protocol The protocol to use
96       * @param encoding The encoding to use
97       * @throws NoSuchAlgorithmException
98       * @since 3.3
99       */
100     public AuthenticatingSMTPClient(String protocol, String encoding) throws NoSuchAlgorithmException {
101         super(protocol, false, encoding);
102     }
103 
104     /***
105      * A convenience method to send the ESMTP EHLO command to the server,
106      * receive the reply, and return the reply code.
107      * <p>
108      * @param hostname The hostname of the sender.
109      * @return The reply code received from the server.
110      * @exception SMTPConnectionClosedException
111      *      If the SMTP server prematurely closes the connection as a result
112      *      of the client being idle or some other reason causing the server
113      *      to send SMTP reply code 421.  This exception may be caught either
114      *      as an IOException or independently as itself.
115      * @exception IOException  If an I/O error occurs while either sending the
116      *      command or receiving the server reply.
117      ***/
118     public int ehlo(String hostname) throws IOException
119     {
120         return sendCommand(SMTPCommand.EHLO, hostname);
121     }
122 
123     /***
124      * Login to the ESMTP server by sending the EHLO command with the
125      * given hostname as an argument.  Before performing any mail commands,
126      * you must first login.
127      * <p>
128      * @param hostname  The hostname with which to greet the SMTP server.
129      * @return True if successfully completed, false if not.
130      * @exception SMTPConnectionClosedException
131      *      If the SMTP server prematurely closes the connection as a result
132      *      of the client being idle or some other reason causing the server
133      *      to send SMTP reply code 421.  This exception may be caught either
134      *      as an IOException or independently as itself.
135      * @exception IOException  If an I/O error occurs while either sending a
136      *      command to the server or receiving a reply from the server.
137      ***/
138     public boolean elogin(String hostname) throws IOException
139     {
140         return SMTPReply.isPositiveCompletion(ehlo(hostname));
141     }
142 
143 
144     /***
145      * Login to the ESMTP server by sending the EHLO command with the
146      * client hostname as an argument.  Before performing any mail commands,
147      * you must first login.
148      * <p>
149      * @return True if successfully completed, false if not.
150      * @exception SMTPConnectionClosedException
151      *      If the SMTP server prematurely closes the connection as a result
152      *      of the client being idle or some other reason causing the server
153      *      to send SMTP reply code 421.  This exception may be caught either
154      *      as an IOException or independently as itself.
155      * @exception IOException  If an I/O error occurs while either sending a
156      *      command to the server or receiving a reply from the server.
157      ***/
158     public boolean elogin() throws IOException
159     {
160         String name;
161         InetAddress host;
162 
163         host = getLocalAddress();
164         name = host.getHostName();
165 
166         if (name == null) {
167             return false;
168         }
169 
170         return SMTPReply.isPositiveCompletion(ehlo(name));
171     }
172 
173     /***
174      * Returns the integer values of the enhanced reply code of the last SMTP reply.
175      * @return The integer values of the enhanced reply code of the last SMTP reply.
176      *  First digit is in the first array element.
177      ***/
178     public int[] getEnhancedReplyCode()
179     {
180         String reply = getReplyString().substring(4);
181         String[] parts = reply.substring(0, reply.indexOf(' ')).split ("\\.");
182         int[] res = new int[parts.length];
183         for (int i = 0; i < parts.length; i++)
184         {
185             res[i] = Integer.parseInt (parts[i]);
186         }
187         return res;
188     }
189 
190     /***
191      * Authenticate to the SMTP server by sending the AUTH command with the
192      * selected mechanism, using the given username and the given password.
193      *
194      * @param method the method to use, one of the {@link AuthenticatingSMTPClient.AUTH_METHOD} enum values
195      * @param username the user name.
196      *        If the method is XOAUTH, then this is used as the plain text oauth protocol parameter string
197      *        which is Base64-encoded for transmission.
198      * @param password the password for the username.
199      *        Ignored for XOAUTH.
200      *
201      * @return True if successfully completed, false if not.
202      * @exception SMTPConnectionClosedException
203      *      If the SMTP server prematurely closes the connection as a result
204      *      of the client being idle or some other reason causing the server
205      *      to send SMTP reply code 421.  This exception may be caught either
206      *      as an IOException or independently as itself.
207      * @exception IOException  If an I/O error occurs while either sending a
208      *      command to the server or receiving a reply from the server.
209      * @exception NoSuchAlgorithmException If the CRAM hash algorithm
210      *      cannot be instantiated by the Java runtime system.
211      * @exception InvalidKeyException If the CRAM hash algorithm
212      *      failed to use the given password.
213      * @exception InvalidKeySpecException If the CRAM hash algorithm
214      *      failed to use the given password.
215      ***/
216     public boolean auth(AuthenticatingSMTPClient.AUTH_METHOD method,
217                         String username, String password)
218                         throws IOException, NoSuchAlgorithmException,
219                         InvalidKeyException, InvalidKeySpecException
220     {
221         if (!SMTPReply.isPositiveIntermediate(sendCommand(SMTPCommand.AUTH,
222                 AUTH_METHOD.getAuthName(method)))) {
223             return false;
224         }
225 
226         if (method.equals(AUTH_METHOD.PLAIN))
227         {
228             // the server sends an empty response ("334 "), so we don't have to read it.
229             return SMTPReply.isPositiveCompletion(sendCommand(
230                     Base64.encodeBase64StringUnChunked(("\000" + username + "\000" + password).getBytes(getCharsetName())) // Java 1.6 can use getCharset()
231                 ));
232         }
233         else if (method.equals(AUTH_METHOD.CRAM_MD5))
234         {
235             // get the CRAM challenge
236             byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(4).trim());
237             // get the Mac instance
238             Mac hmac_md5 = Mac.getInstance("HmacMD5");
239             hmac_md5.init(new SecretKeySpec(password.getBytes(getCharsetName()), "HmacMD5")); // Java 1.6 can use getCharset()
240             // compute the result:
241             byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes(getCharsetName()); // Java 1.6 can use getCharset()
242             // join the byte arrays to form the reply
243             byte[] usernameBytes = username.getBytes(getCharsetName()); // Java 1.6 can use getCharset()
244             byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
245             System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
246             toEncode[usernameBytes.length] = ' ';
247             System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
248             // send the reply and read the server code:
249             return SMTPReply.isPositiveCompletion(sendCommand(
250                 Base64.encodeBase64StringUnChunked(toEncode)));
251         }
252         else if (method.equals(AUTH_METHOD.LOGIN))
253         {
254             // the server sends fixed responses (base64("Username") and
255             // base64("Password")), so we don't have to read them.
256             if (!SMTPReply.isPositiveIntermediate(sendCommand(
257                 Base64.encodeBase64StringUnChunked(username.getBytes(getCharsetName()))))) { // Java 1.6 can use getCharset()
258                 return false;
259             }
260             return SMTPReply.isPositiveCompletion(sendCommand(
261                 Base64.encodeBase64StringUnChunked(password.getBytes(getCharsetName())))); // Java 1.6 can use getCharset()
262         }
263         else if (method.equals(AUTH_METHOD.XOAUTH))
264         {
265             return SMTPReply.isPositiveIntermediate(sendCommand(
266                     Base64.encodeBase64StringUnChunked(username.getBytes(getCharsetName())) // Java 1.6 can use getCharset()
267             ));
268         } else {
269             return false; // safety check
270         }
271     }
272 
273     /**
274      * Converts the given byte array to a String containing the hex values of the bytes.
275      * For example, the byte 'A' will be converted to '41', because this is the ASCII code
276      * (and the byte value) of the capital letter 'A'.
277      * @param a The byte array to convert.
278      * @return The resulting String of hex codes.
279      */
280     private String _convertToHexString(byte[] a)
281     {
282         StringBuilder result = new StringBuilder(a.length*2);
283         for (byte element : a)
284         {
285             if ( (element & 0x0FF) <= 15 ) {
286                 result.append("0");
287             }
288             result.append(Integer.toHexString(element & 0x0FF));
289         }
290         return result.toString();
291     }
292 
293     /**
294      * The enumeration of currently-supported authentication methods.
295      */
296     public static enum AUTH_METHOD
297     {
298         /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
299         PLAIN,
300         /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
301         CRAM_MD5,
302         /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
303         LOGIN,
304         /** XOAuth method which accepts a signed and base64ed OAuth URL. */
305         XOAUTH;
306 
307         /**
308          * Gets the name of the given authentication method suitable for the server.
309          * @param method The authentication method to get the name for.
310          * @return The name of the given authentication method suitable for the server.
311          */
312         public static final String getAuthName(AUTH_METHOD method)
313         {
314             if (method.equals(AUTH_METHOD.PLAIN)) {
315                 return "PLAIN";
316             } else if (method.equals(AUTH_METHOD.CRAM_MD5)) {
317                 return "CRAM-MD5";
318             } else if (method.equals(AUTH_METHOD.LOGIN)) {
319                 return "LOGIN";
320             } else if (method.equals(AUTH_METHOD.XOAUTH)) {
321                 return "XOAUTH";
322             } else {
323                 return null;
324             }
325         }
326     }
327 }
328 
329 /* kate: indent-width 4; replace-tabs on; */