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 018 package org.apache.commons.net.smtp; 019 020 import java.io.IOException; 021 import java.net.InetAddress; 022 import java.security.InvalidKeyException; 023 import java.security.NoSuchAlgorithmException; 024 import java.security.spec.InvalidKeySpecException; 025 import javax.crypto.Mac; 026 import javax.crypto.spec.SecretKeySpec; 027 028 import org.apache.commons.net.util.Base64; 029 030 031 /** 032 * An SMTP Client class with authentication support (RFC4954). 033 * 034 * @see SMTPClient 035 * @since 3.0 036 */ 037 public class AuthenticatingSMTPClient extends SMTPSClient 038 { 039 /** 040 * The default AuthenticatingSMTPClient constructor. 041 * Creates a new Authenticating SMTP Client. 042 * @throws NoSuchAlgorithmException 043 */ 044 public AuthenticatingSMTPClient() throws NoSuchAlgorithmException 045 { 046 super(); 047 } 048 049 /** 050 * Overloaded constructor that takes a protocol specification 051 * @param protocol The protocol to use 052 * @throws NoSuchAlgorithmException 053 */ 054 public AuthenticatingSMTPClient(String protocol) throws NoSuchAlgorithmException { 055 super(protocol); 056 } 057 058 /*** 059 * A convenience method to send the ESMTP EHLO command to the server, 060 * receive the reply, and return the reply code. 061 * <p> 062 * @param hostname The hostname of the sender. 063 * @return The reply code received from the server. 064 * @exception SMTPConnectionClosedException 065 * If the SMTP server prematurely closes the connection as a result 066 * of the client being idle or some other reason causing the server 067 * to send SMTP reply code 421. This exception may be caught either 068 * as an IOException or independently as itself. 069 * @exception IOException If an I/O error occurs while either sending the 070 * command or receiving the server reply. 071 ***/ 072 public int ehlo(String hostname) throws IOException 073 { 074 return sendCommand(SMTPCommand.EHLO, hostname); 075 } 076 077 /*** 078 * Login to the ESMTP server by sending the EHLO command with the 079 * given hostname as an argument. Before performing any mail commands, 080 * you must first login. 081 * <p> 082 * @param hostname The hostname with which to greet the SMTP server. 083 * @return True if successfully completed, false if not. 084 * @exception SMTPConnectionClosedException 085 * If the SMTP server prematurely closes the connection as a result 086 * of the client being idle or some other reason causing the server 087 * to send SMTP reply code 421. This exception may be caught either 088 * as an IOException or independently as itself. 089 * @exception IOException If an I/O error occurs while either sending a 090 * command to the server or receiving a reply from the server. 091 ***/ 092 public boolean elogin(String hostname) throws IOException 093 { 094 return SMTPReply.isPositiveCompletion(ehlo(hostname)); 095 } 096 097 098 /*** 099 * Login to the ESMTP server by sending the EHLO command with the 100 * client hostname as an argument. Before performing any mail commands, 101 * you must first login. 102 * <p> 103 * @return True if successfully completed, false if not. 104 * @exception SMTPConnectionClosedException 105 * If the SMTP server prematurely closes the connection as a result 106 * of the client being idle or some other reason causing the server 107 * to send SMTP reply code 421. This exception may be caught either 108 * as an IOException or independently as itself. 109 * @exception IOException If an I/O error occurs while either sending a 110 * command to the server or receiving a reply from the server. 111 ***/ 112 public boolean elogin() throws IOException 113 { 114 String name; 115 InetAddress host; 116 117 host = getLocalAddress(); 118 name = host.getHostName(); 119 120 if (name == null) { 121 return false; 122 } 123 124 return SMTPReply.isPositiveCompletion(ehlo(name)); 125 } 126 127 /*** 128 * Returns the integer values of the enhanced reply code of the last SMTP reply. 129 * @return The integer values of the enhanced reply code of the last SMTP reply. 130 * First digit is in the first array element. 131 ***/ 132 public int[] getEnhancedReplyCode() 133 { 134 String reply = getReplyString().substring(4); 135 String[] parts = reply.substring(0, reply.indexOf(' ')).split ("\\."); 136 int[] res = new int[parts.length]; 137 for (int i = 0; i < parts.length; i++) 138 { 139 res[i] = Integer.parseInt (parts[i]); 140 } 141 return res; 142 } 143 144 /*** 145 * Authenticate to the SMTP server by sending the AUTH command with the 146 * selected mechanism, using the given username and the given password. 147 * 148 * @param method the method to use, one of the {@link AuthenticatingSMTPClient.AUTH_METHOD} enum values 149 * @param username the user name. 150 * If the method is XOAUTH, then this is used as the plain text oauth protocol parameter string 151 * which is Base64-encoded for transmission. 152 * @param password the password for the username. 153 * Ignored for XOAUTH. 154 * 155 * @return True if successfully completed, false if not. 156 * @exception SMTPConnectionClosedException 157 * If the SMTP server prematurely closes the connection as a result 158 * of the client being idle or some other reason causing the server 159 * to send SMTP reply code 421. This exception may be caught either 160 * as an IOException or independently as itself. 161 * @exception IOException If an I/O error occurs while either sending a 162 * command to the server or receiving a reply from the server. 163 * @exception NoSuchAlgorithmException If the CRAM hash algorithm 164 * cannot be instantiated by the Java runtime system. 165 * @exception InvalidKeyException If the CRAM hash algorithm 166 * failed to use the given password. 167 * @exception InvalidKeySpecException If the CRAM hash algorithm 168 * failed to use the given password. 169 ***/ 170 public boolean auth(AuthenticatingSMTPClient.AUTH_METHOD method, 171 String username, String password) 172 throws IOException, NoSuchAlgorithmException, 173 InvalidKeyException, InvalidKeySpecException 174 { 175 if (!SMTPReply.isPositiveIntermediate(sendCommand(SMTPCommand.AUTH, 176 AUTH_METHOD.getAuthName(method)))) { 177 return false; 178 } 179 180 if (method.equals(AUTH_METHOD.PLAIN)) 181 { 182 // the server sends an empty response ("334 "), so we don't have to read it. 183 return SMTPReply.isPositiveCompletion(sendCommand( 184 Base64.encodeBase64StringUnChunked(("\000" + username + "\000" + password).getBytes()) 185 )); 186 } 187 else if (method.equals(AUTH_METHOD.CRAM_MD5)) 188 { 189 // get the CRAM challenge 190 byte[] serverChallenge = Base64.decodeBase64(getReplyString().substring(4).trim()); 191 // get the Mac instance 192 Mac hmac_md5 = Mac.getInstance("HmacMD5"); 193 hmac_md5.init(new SecretKeySpec(password.getBytes(), "HmacMD5")); 194 // compute the result: 195 byte[] hmacResult = _convertToHexString(hmac_md5.doFinal(serverChallenge)).getBytes(); 196 // join the byte arrays to form the reply 197 byte[] usernameBytes = username.getBytes(); 198 byte[] toEncode = new byte[usernameBytes.length + 1 /* the space */ + hmacResult.length]; 199 System.arraycopy(usernameBytes, 0, toEncode, 0, usernameBytes.length); 200 toEncode[usernameBytes.length] = ' '; 201 System.arraycopy(hmacResult, 0, toEncode, usernameBytes.length + 1, hmacResult.length); 202 // send the reply and read the server code: 203 return SMTPReply.isPositiveCompletion(sendCommand( 204 Base64.encodeBase64StringUnChunked(toEncode))); 205 } 206 else if (method.equals(AUTH_METHOD.LOGIN)) 207 { 208 // the server sends fixed responses (base64("Username") and 209 // base64("Password")), so we don't have to read them. 210 if (!SMTPReply.isPositiveIntermediate(sendCommand( 211 Base64.encodeBase64StringUnChunked(username.getBytes())))) { 212 return false; 213 } 214 return SMTPReply.isPositiveCompletion(sendCommand( 215 Base64.encodeBase64StringUnChunked(password.getBytes()))); 216 } 217 else if (method.equals(AUTH_METHOD.XOAUTH)) 218 { 219 return SMTPReply.isPositiveIntermediate(sendCommand( 220 Base64.encodeBase64StringUnChunked(username.getBytes()) 221 )); 222 } else { 223 return false; // safety check 224 } 225 } 226 227 /** 228 * Converts the given byte array to a String containing the hex values of the bytes. 229 * For example, the byte 'A' will be converted to '41', because this is the ASCII code 230 * (and the byte value) of the capital letter 'A'. 231 * @param a The byte array to convert. 232 * @return The resulting String of hex codes. 233 */ 234 private String _convertToHexString(byte[] a) 235 { 236 StringBuilder result = new StringBuilder(a.length*2); 237 for (byte element : a) 238 { 239 if ( (element & 0x0FF) <= 15 ) { 240 result.append("0"); 241 } 242 result.append(Integer.toHexString(element & 0x0FF)); 243 } 244 return result.toString(); 245 } 246 247 /** 248 * The enumeration of currently-supported authentication methods. 249 */ 250 public static enum AUTH_METHOD 251 { 252 /** The standarised (RFC4616) PLAIN method, which sends the password unencrypted (insecure). */ 253 PLAIN, 254 /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */ 255 CRAM_MD5, 256 /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */ 257 LOGIN, 258 /** XOAuth method which accepts a signed and base64ed OAuth URL. */ 259 XOAUTH; 260 261 /** 262 * Gets the name of the given authentication method suitable for the server. 263 * @param method The authentication method to get the name for. 264 * @return The name of the given authentication method suitable for the server. 265 */ 266 public static final String getAuthName(AUTH_METHOD method) 267 { 268 if (method.equals(AUTH_METHOD.PLAIN)) { 269 return "PLAIN"; 270 } else if (method.equals(AUTH_METHOD.CRAM_MD5)) { 271 return "CRAM-MD5"; 272 } else if (method.equals(AUTH_METHOD.LOGIN)) { 273 return "LOGIN"; 274 } else if (method.equals(AUTH_METHOD.XOAUTH)) { 275 return "XOAUTH"; 276 } else { 277 return null; 278 } 279 } 280 } 281 } 282 283 /* kate: indent-width 4; replace-tabs on; */