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       */
44      public AuthenticatingSMTPClient()
45      {
46          super();
47      }
48  
49      /**
50       * Overloaded constructor that takes a protocol specification
51       * @param protocol The protocol to use
52       */
53      public AuthenticatingSMTPClient(String protocol) {
54          super(protocol);
55      }
56  
57      /**
58       * Overloaded constructor that takes a protocol specification and the implicit argument
59       * @param proto the protocol.
60       * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
61       * @since 3.3
62       */
63      public AuthenticatingSMTPClient(String proto, boolean implicit)
64      {
65        super(proto, implicit);
66      }
67  
68      /**
69       * Overloaded constructor that takes the protocol specification, the implicit argument and encoding
70       * @param proto the protocol.
71       * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
72       * @param encoding the encoding
73       * @since 3.3
74       */
75      public AuthenticatingSMTPClient(String proto, boolean implicit, String encoding)
76      {
77        super(proto, implicit, encoding);
78      }
79  
80      /**
81       * Overloaded constructor that takes the implicit argument, and using {@link #DEFAULT_PROTOCOL} i.e. TLS
82       * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit
83       * @param ctx A pre-configured SSL Context.
84       * @since 3.3
85       */
86      public AuthenticatingSMTPClient(boolean implicit, SSLContext ctx)
87      {
88        super(implicit, ctx);
89      }
90  
91      /**
92       * Overloaded constructor that takes a protocol specification and encoding
93       * @param protocol The protocol to use
94       * @param encoding The encoding to use
95       * @since 3.3
96       */
97      public AuthenticatingSMTPClient(String protocol, String encoding) {
98          super(protocol, false, encoding);
99      }
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      * @exception 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      * @exception 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      * @exception 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      * @exception 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      * @exception 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      * @exception 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      * @exception 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      * @exception IOException  If an I/O error occurs while either sending a
205      *      command to the server or receiving a reply from the server.
206      * @exception NoSuchAlgorithmException If the CRAM hash algorithm
207      *      cannot be instantiated by the Java runtime system.
208      * @exception InvalidKeyException If the CRAM hash algorithm
209      *      failed to use the given password.
210      * @exception 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                     // Java 1.6 can use getCharset()
228                     Base64.encodeBase64StringUnChunked(("\000" + username + "\000" + password).getBytes(getCharsetName()))
229                 ));
230         }
231         else if (method.equals(AUTH_METHOD.CRAM_MD5))
232         {
233             // get the CRAM challenge
234             byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(4).trim());
235             // get the Mac instance
236             Mac hmac_md5 = Mac.getInstance("HmacMD5");
237             hmac_md5.init(new SecretKeySpec(password.getBytes(getCharsetName()), "HmacMD5")); // Java 1.6 can use getCharset()
238             // compute the result:
239             // Java 1.6 can use getCharset()
240             byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes(getCharsetName());
241             // join the byte arrays to form the reply
242             byte[] usernameBytes = username.getBytes(getCharsetName()); // Java 1.6 can use getCharset()
243             byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length];
244             System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length);
245             toEncode[usernameBytes.length] = ' ';
246             System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length);
247             // send the reply and read the server code:
248             return SMTPReply.isPositiveCompletion(sendCommand(
249                 Base64.encodeBase64StringUnChunked(toEncode)));
250         }
251         else if (method.equals(AUTH_METHOD.LOGIN))
252         {
253             // the server sends fixed responses (base64("Username") and
254             // base64("Password")), so we don't have to read them.
255             if (!SMTPReply.isPositiveIntermediate(sendCommand(
256                 Base64.encodeBase64StringUnChunked(username.getBytes(getCharsetName()))))) { // Java 1.6 can use getCharset()
257                 return false;
258             }
259             return SMTPReply.isPositiveCompletion(sendCommand(
260                 Base64.encodeBase64StringUnChunked(password.getBytes(getCharsetName())))); // Java 1.6 can use getCharset()
261         }
262         else if (method.equals(AUTH_METHOD.XOAUTH))
263         {
264             return SMTPReply.isPositiveIntermediate(sendCommand(
265                     Base64.encodeBase64StringUnChunked(username.getBytes(getCharsetName())) // Java 1.6 can use getCharset()
266             ));
267         } else {
268             return false; // safety check
269         }
270     }
271 
272     /**
273      * Converts the given byte array to a String containing the hex values of the bytes.
274      * For example, the byte 'A' will be converted to '41', because this is the ASCII code
275      * (and the byte value) of the capital letter 'A'.
276      * @param a The byte array to convert.
277      * @return The resulting String of hex codes.
278      */
279     private String _convertToHexString(byte[] a)
280     {
281         StringBuilder result = new StringBuilder(a.length*2);
282         for (byte element : a)
283         {
284             if ( (element & 0x0FF) <= 15 ) {
285                 result.append("0");
286             }
287             result.append(Integer.toHexString(element & 0x0FF));
288         }
289         return result.toString();
290     }
291 
292     /**
293      * The enumeration of currently-supported authentication methods.
294      */
295     public static enum AUTH_METHOD
296     {
297         /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */
298         PLAIN,
299         /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
300         CRAM_MD5,
301         /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
302         LOGIN,
303         /** XOAuth method which accepts a signed and base64ed OAuth URL. */
304         XOAUTH;
305 
306         /**
307          * Gets the name of the given authentication method suitable for the server.
308          * @param method The authentication method to get the name for.
309          * @return The name of the given authentication method suitable for the server.
310          */
311         public static final String getAuthName(AUTH_METHOD method)
312         {
313             if (method.equals(AUTH_METHOD.PLAIN)) {
314                 return "PLAIN";
315             } else if (method.equals(AUTH_METHOD.CRAM_MD5)) {
316                 return "CRAM-MD5";
317             } else if (method.equals(AUTH_METHOD.LOGIN)) {
318                 return "LOGIN";
319             } else if (method.equals(AUTH_METHOD.XOAUTH)) {
320                 return "XOAUTH";
321             } else {
322                 return null;
323             }
324         }
325     }
326 }
327 
328 /* kate: indent-width 4; replace-tabs on; */