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