/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.scripting.mail; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetAddress; import java.net.Socket; import java.util.ArrayList; import javax.net.ssl.SSLSocket; import com.unboundid.util.Base64; import com.slamd.asn1.ASN1Element; import com.slamd.jobs.JSSEBlindTrustSocketFactory; import com.slamd.job.JobClass; import com.slamd.scripting.engine.Argument; import com.slamd.scripting.engine.Method; import com.slamd.scripting.engine.ScriptException; import com.slamd.scripting.engine.Variable; import com.slamd.scripting.general.BooleanVariable; import com.slamd.scripting.general.IntegerVariable; import com.slamd.scripting.general.StringVariable; /** * This class defines a variable that maintains a connection to an SMTP mail * server and allows for interaction with that server. An SMTP connection has * the following methods: * * <UL> * <LI>authCRAMMD5(string username, string password) -- Authenticates to the * SMTP server using the CRAM-MD5 mechanism. Returns a Boolean value * indicating whether the authentication was successful.</LI> * <LI>authLogin(string username, string password) -- Authenticates to the * SMTP server using the AUTH LOGIN mechanism. Returns a Boolean value * indicating whether the authentication was successful.</LI> * <LI>authPlain(string username, string password) -- Authenticates to the * SMTP server using the AUTH PLAIN mechanism. Returns a Boolean value * indicating whether the authentication was successful.</LI> * <LI>connect(string host, int port) -- Establishes an SMTP connection to the * mail server. Returns a Boolean value indicating whether the connection * was established successfully.</LI> * <LI>connect(string host, int port, boolean useSSL) -- Establishes an SMTP * connection to the mail server, optionally using SSL. Returns a Boolean * value indicating whether the connection was established * successfully.</LI> * <LI>disconnect() -- Closes the connection to the SMTP server. This method * does not return a value.</LI> * <LI>getFailureReason() -- Retrieves a string that provides information * about the reason for the last failure, if that is available.</LI> * <LI>noOp() -- Sends a NOOP command to the server, which has no effect but * to prevent the connection from remaining idle for too long. This * method does not return a value.</LI> * <LI>send(MailMessage message) -- Sends the provided mail message to the * server. This method returns a Boolean value indicating whether the * message was sent successfully.</LI> * <LI>sendCommand(string command) -- Sends the specified command to the SMTP * server and retrieves the response line from the server.</LI> * </UL> * * * @author Neil A. Wilson */ public class SMTPConnectionVariable extends Variable { /** * The name that will be used for the data type of SMTP connection variables. */ public static final String SMTP_CONNECTION_VARIABLE_TYPE = "smtpconnection"; /** * The name of the method that can be used to authenticate to the server using * CRAM-MD5. */ public static final String AUTH_CRAM_MD5_METHOD_NAME = "authcrammd5"; /** * The method number for the "authCRAMMD5" method. */ public static final int AUTH_CRAM_MD5_METHOD_NUMBER = 0; /** * The name of the method that can be used to authenticate to the server using * AUTH LOGIN. */ public static final String AUTH_LOGIN_METHOD_NAME = "authlogin"; /** * The method number for the "authLogin" method. */ public static final int AUTH_LOGIN_METHOD_NUMBER = 1; /** * The name of the method that can be used to authenticate to the server using * AUTH PLAIN. */ public static final String AUTH_PLAIN_METHOD_NAME = "authplain"; /** * The method number for the "authPlain" method. */ public static final int AUTH_PLAIN_METHOD_NUMBER = 2; /** * The name of the method that can be used to establish a connection to an * SMTP server. */ public static final String CONNECT_METHOD_NAME = "connect"; /** * The method number for the first "connect" method. */ public static final int CONNECT_1_METHOD_NUMBER = 3; /** * The method number for the second "connect" method. */ public static final int CONNECT_2_METHOD_NUMBER = 4; /** * The name of the method that can be used to disconnect from an SMTP server. */ public static final String DISCONNECT_METHOD_NAME = "disconnect"; /** * The method number for the "disconnect" method. */ public static final int DISCONNECT_METHOD_NUMBER = 5; /** * The name of the method that can be used to determine the reason that the * last operation failed. */ public static final String GET_FAILURE_REASON_METHOD_NAME = "getfailurereason"; /** * The method number for the "getFailureReason" method. */ public static final int GET_FAILURE_REASON_METHOD_NUMBER = 6; /** * The name of the method that sends a "NOOP" to the server. */ public static final String NOOP_METHOD_NAME = "noop"; /** * The method number for the "noOp" method. */ public static final int NOOP_METHOD_NUMBER = 7; /** * The name of the method that can be used to send a message to the SMTP * server. */ public static final String SEND_METHOD_NAME = "send"; /** * The method number for the "send" method. */ public static final int SEND_METHOD_NUMBER = 8; /** * The name of the method that can be used to send a raw SMTP command to the * server. */ public static final String SEND_COMMAND_METHOD_NAME = "sendcommand"; /** * The method number for the "sendCommand" method. */ public static final int SEND_COMMAND_METHOD_NUMBER = 9; /** * The set of methods associated with SMTP connection variables. */ public static final Method[] SMTP_CONNECTION_VARIABLE_METHODS = new Method[] { new Method(AUTH_CRAM_MD5_METHOD_NAME, new String[] { StringVariable.STRING_VARIABLE_TYPE, StringVariable.STRING_VARIABLE_TYPE }, BooleanVariable.BOOLEAN_VARIABLE_TYPE), new Method(AUTH_LOGIN_METHOD_NAME, new String[] { StringVariable.STRING_VARIABLE_TYPE, StringVariable.STRING_VARIABLE_TYPE }, BooleanVariable.BOOLEAN_VARIABLE_TYPE), new Method(AUTH_PLAIN_METHOD_NAME, new String[] { StringVariable.STRING_VARIABLE_TYPE, StringVariable.STRING_VARIABLE_TYPE }, BooleanVariable.BOOLEAN_VARIABLE_TYPE), new Method(CONNECT_METHOD_NAME, new String[] { StringVariable.STRING_VARIABLE_TYPE, IntegerVariable.INTEGER_VARIABLE_TYPE }, BooleanVariable.BOOLEAN_VARIABLE_TYPE), new Method(CONNECT_METHOD_NAME, new String[] { StringVariable.STRING_VARIABLE_TYPE, IntegerVariable.INTEGER_VARIABLE_TYPE, BooleanVariable.BOOLEAN_VARIABLE_TYPE }, BooleanVariable.BOOLEAN_VARIABLE_TYPE), new Method(DISCONNECT_METHOD_NAME, new String[0], null), new Method(GET_FAILURE_REASON_METHOD_NAME, new String[0], StringVariable.STRING_VARIABLE_TYPE), new Method(NOOP_METHOD_NAME, new String[0], null), new Method(SEND_METHOD_NAME, new String[] { MailMessageVariable.MAIL_MESSAGE_VARIABLE_TYPE }, BooleanVariable.BOOLEAN_VARIABLE_TYPE), new Method(SEND_COMMAND_METHOD_NAME, new String[] { StringVariable.STRING_VARIABLE_TYPE }, StringVariable.STRING_VARIABLE_TYPE) }; /** * The end of line character as required by RFC 821. */ public static final String EOL = "\r\n"; // The socket, reader, and writer used to communicate with the SMTP server. private BufferedReader reader; private BufferedWriter writer; private Socket socket; // The reason that the last operation failed. private String failureReason; /** * Creates a new variable with no name, to be used only when creating a * variable with <CODE>Class.newInstance()</CODE>, and only when * <CODE>setName()</CODE> is called after that to set the name. * * @throws ScriptException If a problem occurs while initializing the new * variable. */ public SMTPConnectionVariable() throws ScriptException { failureReason = null; } /** * Retrieves the name of the variable type for this variable. * * @return The name of the variable type for this variable. */ @Override() public String getVariableTypeName() { return SMTP_CONNECTION_VARIABLE_TYPE; } /** * Retrieves a list of all methods defined for this variable. * * @return A list of all methods defined for this variable. */ @Override() public Method[] getMethods() { return SMTP_CONNECTION_VARIABLE_METHODS; } /** * Indicates whether this variable type has a method with the specified name. * * @param methodName The name of the method. * * @return <CODE>true</CODE> if this variable has a method with the specified * name, or <CODE>false</CODE> if it does not. */ @Override() public boolean hasMethod(String methodName) { for (int i=0; i < SMTP_CONNECTION_VARIABLE_METHODS.length; i++) { if (SMTP_CONNECTION_VARIABLE_METHODS[i].getName().equals(methodName)) { return true; } } return false; } /** * Retrieves the method number for the method that has the specified name and * argument types, or -1 if there is no such method. * * @param methodName The name of the method. * @param argumentTypes The list of argument types for the method. * * @return The method number for the method that has the specified name and * argument types. */ @Override() public int getMethodNumber(String methodName, String[] argumentTypes) { for (int i=0; i < SMTP_CONNECTION_VARIABLE_METHODS.length; i++) { if (SMTP_CONNECTION_VARIABLE_METHODS[i].hasSignature(methodName, argumentTypes)) { return i; } } return -1; } /** * Retrieves the return type for the method with the specified name and * argument types. * * @param methodName The name of the method. * @param argumentTypes The set of argument types for the method. * * @return The return type for the method, or <CODE>null</CODE> if there is * no such method defined. */ @Override() public String getReturnTypeForMethod(String methodName, String[] argumentTypes) { for (int i=0; i < SMTP_CONNECTION_VARIABLE_METHODS.length; i++) { if (SMTP_CONNECTION_VARIABLE_METHODS[i].hasSignature(methodName, argumentTypes)) { return SMTP_CONNECTION_VARIABLE_METHODS[i].getReturnType(); } } return null; } /** * Executes the specified method, using the provided variables as arguments * to the method, and makes the return value available to the caller. * * @param lineNumber The line number of the script in which the method * call occurs. * @param methodNumber The method number of the method to execute. * @param arguments The set of arguments to use for the method. * * @return The value returned from the method, or <CODE>null</CODE> if it * does not return a value. * * @throws ScriptException If the specified method does not exist, or if a * problem occurs while attempting to execute it. */ @Override() public Variable executeMethod(int lineNumber, int methodNumber, Argument[] arguments) throws ScriptException { switch (methodNumber) { case AUTH_CRAM_MD5_METHOD_NUMBER: StringVariable sv1 = (StringVariable) arguments[0].getArgumentValue(); StringVariable sv2 = (StringVariable) arguments[1].getArgumentValue(); failureReason = null; try { writer.write("AUTH CRAM-MD5" + EOL); writer.flush(); String line = reader.readLine(); if (! line.startsWith("334 ")) { failureReason = "Server rejected AUTH CRAM-MD5 (response was " + line + ')'; return new BooleanVariable(false); } String challenge = line.substring(4).trim(); CRAMMD5Handler handler = new CRAMMD5Handler(); String response = handler.generateCRAMMD5Response(sv1.getStringValue(), sv2.getStringValue(), challenge); writer.write(response + EOL); writer.flush(); line = reader.readLine(); if (! line.startsWith("235 ")) { failureReason = "Server rejected CRAM-MD5 challenge response " + "(response was " + line + ')'; return new BooleanVariable(false); } return new BooleanVariable(true); } catch (Exception e) { failureReason = "Caught exception: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } case AUTH_LOGIN_METHOD_NUMBER: sv1 = (StringVariable) arguments[0].getArgumentValue(); sv2 = (StringVariable) arguments[1].getArgumentValue(); failureReason = null; try { writer.write("AUTH LOGIN" + EOL); writer.flush(); String line = reader.readLine(); if (! line.startsWith("334 ")) { failureReason = "Server rejected AUTH LOGIN (response was " + line + ')'; return new BooleanVariable(false); } byte[] usernameBytes = ASN1Element.getBytes(sv1.getStringValue()); String usernameString = Base64.encode(usernameBytes); writer.write(usernameString + EOL); writer.flush(); line = reader.readLine(); if (! line.startsWith("334 ")) { failureReason = "Server rejected username (response was " + line + ')'; return new BooleanVariable(false); } byte[] passwordBytes = ASN1Element.getBytes(sv2.getStringValue()); String passwordString = Base64.encode(passwordBytes); writer.write(passwordString + EOL); writer.flush(); line = reader.readLine(); if (line.startsWith("235 ")) { return new BooleanVariable(true); } else { failureReason = "Server rejected credentials (response was " + line + ')'; return new BooleanVariable(false); } } catch (Exception e) { failureReason = "Caught exception: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } case AUTH_PLAIN_METHOD_NUMBER: sv1 = (StringVariable) arguments[0].getArgumentValue(); sv2 = (StringVariable) arguments[1].getArgumentValue(); failureReason = null; try { writer.write("AUTH PLAIN" + EOL); writer.flush(); String line = reader.readLine(); if (! line.startsWith("334 ")) { failureReason = "Server rejected AUTH PLAIN (response was " + line + ')'; return new BooleanVariable(false); } byte[] usernameBytes = ASN1Element.getBytes(sv1.getStringValue()); byte[] passwordBytes = ASN1Element.getBytes(sv2.getStringValue()); byte[] authBytes = new byte[usernameBytes.length + passwordBytes.length + 2]; System.arraycopy(usernameBytes, 0, authBytes, 1, usernameBytes.length); System.arraycopy(passwordBytes, 0, authBytes, usernameBytes.length+2, passwordBytes.length); String authString = Base64.encode(authBytes); writer.write(authString + EOL); writer.flush(); line = reader.readLine(); if (line.startsWith("235 ")) { return new BooleanVariable(true); } else { failureReason = "Server rejected credentials (response was " + line + ')'; return new BooleanVariable(false); } } catch (Exception e) { failureReason = "Caught exception: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } case CONNECT_1_METHOD_NUMBER: sv1 = (StringVariable) arguments[0].getArgumentValue(); IntegerVariable iv1 = (IntegerVariable) arguments[1].getArgumentValue(); String address = sv1.getStringValue(); int port = iv1.getIntValue(); failureReason = null; try { socket = new Socket(address, port); reader = new BufferedReader(new InputStreamReader( socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter( socket.getOutputStream())); String line = reader.readLine(); if ((line == null) || (! line.startsWith("220"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading greeting"; } else { failureReason = "Error reading greeting: " + line; } reader.close(); writer.close(); socket.close(); return new BooleanVariable(false); } String clientAddress = InetAddress.getLocalHost().getHostAddress(); writer.write("HELO [" + clientAddress + ']' + EOL); writer.flush(); line = reader.readLine(); if ((line == null) || (! line.startsWith("250"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading HELO response"; } else { failureReason = "Error reading HELO response: " + line; } reader.close(); writer.close(); socket.close(); return new BooleanVariable(false); } return new BooleanVariable(true); } catch (Exception e) { failureReason = "Caught exception: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } case CONNECT_2_METHOD_NUMBER: sv1 = (StringVariable) arguments[0].getArgumentValue(); iv1 = (IntegerVariable) arguments[1].getArgumentValue(); BooleanVariable bv1 = (BooleanVariable) arguments[2].getArgumentValue(); address = sv1.getStringValue(); port = iv1.getIntValue(); boolean useSSL = bv1.getBooleanValue(); failureReason = null; try { if (useSSL) { JSSEBlindTrustSocketFactory socketFactory = new JSSEBlindTrustSocketFactory(); socket = socketFactory.makeSocket(address, port); } else { socket = new Socket(address, port); } reader = new BufferedReader(new InputStreamReader( socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter( socket.getOutputStream())); String line = reader.readLine(); if ((line == null) || (! line.startsWith("220"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading greeting"; } else { failureReason = "Error reading greeting: " + line; } reader.close(); writer.close(); socket.close(); return new BooleanVariable(false); } String clientAddress = InetAddress.getLocalHost().getHostAddress(); writer.write("HELO [" + clientAddress + ']' + EOL); writer.flush(); line = reader.readLine(); if ((line == null) || (! line.startsWith("250"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading HELO response"; } else { failureReason = "Error reading HELO response: " + line; } reader.close(); writer.close(); socket.close(); return new BooleanVariable(false); } return new BooleanVariable(true); } catch (Exception e) { failureReason = "Caught exception: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } case DISCONNECT_METHOD_NUMBER: failureReason = null; try { writer.write("QUIT" + EOL); writer.flush(); } catch (Exception e) {} try { reader.close(); writer.close(); socket.close(); } catch (Exception e) {} return null; case GET_FAILURE_REASON_METHOD_NUMBER: return new StringVariable(failureReason); case NOOP_METHOD_NUMBER: failureReason = null; try { writer.write("QUIT" + EOL); writer.flush(); reader.readLine(); } catch (Exception e) {} return null; case SEND_METHOD_NUMBER: MailMessageVariable mmv = (MailMessageVariable) arguments[0].getArgumentValue(); failureReason = null; // First, make sure that we have a sender and at least 1 recipient. String sender = mmv.getSender(); String[] recipients = mmv.getRecipients(); if ((sender == null) || (recipients == null) || (recipients.length == 0)) { if (sender == null) { failureReason = "Message has no sender"; } else { failureReason = "Message has no recipients"; } return new BooleanVariable(false); } // Next, send the MAIL FROM command and read the response. try { writer.write("MAIL FROM: "); if (sender.startsWith("<")) { writer.write(sender); } else { writer.write("<"); writer.write(sender); writer.write(">"); } writer.write(EOL); writer.flush(); String line = reader.readLine(); if ((line == null) || (! line.startsWith("250"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading MAIL FROM response"; } else { failureReason = "Error reading MAIL FROM response: " + line; } writer.write("RSET" + EOL); writer.flush(); reader.readLine(); return new BooleanVariable(false); } } catch (Exception e) { failureReason = "Caught exception handling MAIL FROM: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } // Next, send a RCPT TO command for each recipient. try { for (int i=0; i < recipients.length; i++) { writer.write("RCPT TO: "); if (recipients[i].startsWith("<")) { writer.write(recipients[i]); } else { writer.write("<"); writer.write(recipients[i]); writer.write(">"); } writer.write(EOL); writer.flush(); String line = reader.readLine(); if ((line == null) || (! line.startsWith("25"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading RCPT TO response"; } else { failureReason = "Error reading RCPT TO response: " + line; } writer.write("RSET" + EOL); writer.flush(); reader.readLine(); return new BooleanVariable(false); } } } catch (Exception e) { failureReason = "Caught exception handling RCPT TO: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } // Next comes the DATA command for the actual message. try { writer.write("DATA" + EOL); writer.flush(); String line = reader.readLine(); if ((line == null) || (! line.startsWith("354"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading DATA response"; } else { failureReason = "Error reading DATA response " + line; } writer.write("RSET" + EOL); writer.flush(); reader.readLine(); return new BooleanVariable(false); } ArrayList headerLines = mmv.getHeaderLines(); if ((headerLines != null) && (! headerLines.isEmpty())) { for (int i=0; i < headerLines.size(); i++) { writer.write(headerLines.get(i) + EOL); } writer.write(EOL); } ArrayList bodyLines = mmv.getBodyLines(); if ((bodyLines != null) && (! bodyLines.isEmpty())) { for (int i=0; i < bodyLines.size(); i++) { line = (String) bodyLines.get(i); if (line.startsWith(".")) { writer.write("."); } writer.write(line + EOL); } } writer.write('.' + EOL); writer.flush(); line = reader.readLine(); if ((line == null) || (! line.startsWith("250"))) { if (line == null) { failureReason = "Unexpected end of input stream from server " + "reading end of message response"; } else { failureReason = "Error reading end of message response " + line; } writer.write("RSET" + EOL); writer.flush(); reader.readLine(); return new BooleanVariable(false); } } catch (Exception e) { failureReason = "Caught exception while processing DATA: " + JobClass.stackTraceToString(e); return new BooleanVariable(false); } return new BooleanVariable(true); case SEND_COMMAND_METHOD_NUMBER: sv1 = (StringVariable) arguments[0].getArgumentValue(); String command = sv1.getStringValue(); failureReason = null; try { writer.write(command); writer.flush(); return new StringVariable(reader.readLine()); } catch (Exception e) { failureReason = "Caught exception: " + JobClass.stackTraceToString(e); } return new StringVariable(null); default: throw new ScriptException(lineNumber, "There is no method " + methodNumber + " defined for " + getArgumentType() + " variables."); } } /** * Assigns the value of the provided argument to this variable. The value of * the provided argument must be of the same type as this variable. * * @param argument The argument whose value should be assigned to this * variable. * * @throws ScriptException If a problem occurs while performing the * assignment. */ @Override() public void assign(Argument argument) throws ScriptException { if (! argument.getArgumentType().equals(SMTP_CONNECTION_VARIABLE_TYPE)) { throw new ScriptException("Attempt to assign an argument of type " + argument.getArgumentType() + " to a variable of type " + SMTP_CONNECTION_VARIABLE_TYPE + " rejected."); } SMTPConnectionVariable scv = (SMTPConnectionVariable) argument.getArgumentValue(); socket = scv.socket; reader = scv.reader; writer = scv.writer; } /** * Retrieves a string representation of the value of this argument. * * @return A string representation of the value of this argument. */ public String getValueAsString() { if (socket == null) { return "null"; } else { boolean connected = socket.isConnected(); if (! connected) { return "not connected"; } else { String host = socket.getInetAddress().getHostAddress(); int port = socket.getPort(); boolean usingSSL = (socket instanceof SSLSocket); if (usingSSL) { return "smtps://" + host + ';' + port; } else { return "smtp://" + host + ':' + port; } } } } }