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