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 018package org.apache.commons.net.smtp; 019 020import java.io.IOException; 021import java.net.InetAddress; 022import java.security.InvalidKeyException; 023import java.security.NoSuchAlgorithmException; 024import java.security.spec.InvalidKeySpecException; 025import javax.crypto.Mac; 026import javax.crypto.spec.SecretKeySpec; 027import javax.net.ssl.SSLContext; 028 029import org.apache.commons.net.util.Base64; 030 031 032/** 033 * An SMTP Client class with authentication support (RFC4954). 034 * 035 * @see SMTPClient 036 * @since 3.0 037 */ 038public class AuthenticatingSMTPClient extends SMTPSClient 039{ 040 /** 041 * The default AuthenticatingSMTPClient constructor. 042 * Creates a new Authenticating SMTP Client. 043 */ 044 public AuthenticatingSMTPClient() 045 { 046 super(); 047 } 048 049 /** 050 * Overloaded constructor that takes a protocol specification 051 * @param protocol The protocol to use 052 */ 053 public AuthenticatingSMTPClient(String protocol) { 054 super(protocol); 055 } 056 057 /** 058 * Overloaded constructor that takes a protocol specification and the implicit argument 059 * @param proto the protocol. 060 * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit 061 * @since 3.3 062 */ 063 public AuthenticatingSMTPClient(String proto, boolean implicit) 064 { 065 super(proto, implicit); 066 } 067 068 /** 069 * Overloaded constructor that takes the protocol specification, the implicit argument and encoding 070 * @param proto the protocol. 071 * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit 072 * @param encoding the encoding 073 * @since 3.3 074 */ 075 public AuthenticatingSMTPClient(String proto, boolean implicit, String encoding) 076 { 077 super(proto, implicit, encoding); 078 } 079 080 /** 081 * Overloaded constructor that takes the implicit argument, and using {@link #DEFAULT_PROTOCOL} i.e. TLS 082 * @param implicit The security mode, {@code true} for implicit, {@code false} for explicit 083 * @param ctx A pre-configured SSL Context. 084 * @since 3.3 085 */ 086 public AuthenticatingSMTPClient(boolean implicit, SSLContext ctx) 087 { 088 super(implicit, ctx); 089 } 090 091 /** 092 * Overloaded constructor that takes a protocol specification and encoding 093 * @param protocol The protocol to use 094 * @param encoding The encoding to use 095 * @since 3.3 096 */ 097 public AuthenticatingSMTPClient(String protocol, String encoding) { 098 super(protocol, false, encoding); 099 } 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; */