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