You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by pg...@apache.org on 2002/10/01 01:21:05 UTC
cvs commit: jakarta-james/src/java/org/apache/james/smtpserver SMTPHandler.java
pgoldstein 2002/09/30 16:21:05
Modified: src/java/org/apache/james/smtpserver SMTPHandler.java
Log:
A variety of changes to the SMTP handler that are designed to neaten the code as well as
improve adherence to the RFC 2821 specification.
Revision Changes Path
1.28 +643 -432 jakarta-james/src/java/org/apache/james/smtpserver/SMTPHandler.java
Index: SMTPHandler.java
===================================================================
RCS file: /home/cvs/jakarta-james/src/java/org/apache/james/smtpserver/SMTPHandler.java,v
retrieving revision 1.27
retrieving revision 1.28
diff -u -r1.27 -r1.28
--- SMTPHandler.java 26 Sep 2002 01:15:23 -0000 1.27
+++ SMTPHandler.java 30 Sep 2002 23:21:04 -0000 1.28
@@ -6,6 +6,7 @@
* the LICENSE file.
*/
package org.apache.james.smtpserver;
+
import org.apache.avalon.cornerstone.services.connection.ConnectionHandler;
import org.apache.avalon.cornerstone.services.scheduler.PeriodicTimeTrigger;
import org.apache.avalon.cornerstone.services.scheduler.Target;
@@ -55,20 +56,11 @@
// Keys used to store/lookup data in the internal state hash map
- private final static String SERVER_NAME = "SERVER_NAME"; // Local server name
- private final static String SERVER_TYPE = "SERVER_TYPE"; // SMTP Software Type
- private final static String REMOTE_NAME = "REMOTE_NAME"; // Remote host name
- private final static String REMOTE_IP = "REMOTE_IP"; // Remote IP address
- private final static String NAME_GIVEN = "NAME_GIVEN"; // Remote host name provided by
- // client
private final static String CURRENT_HELO_MODE = "CURRENT_HELO_MODE"; // HELO or EHLO
private final static String SENDER = "SENDER_ADDRESS"; // Sender's email address
private final static String MESG_FAILED = "MESG_FAILED"; // Message failed flag
private final static String MESG_SIZE = "MESG_SIZE"; // The size of the message
private final static String RCPT_VECTOR = "RCPT_VECTOR"; // The message recipients
- private final static String SMTP_ID = "SMTP_ID"; // The SMTP ID associated with
- // the connection
- private final static String AUTH = "AUTHENTICATED"; // The authenticated user id
/**
* The character array that indicates termination of an SMTP connection
@@ -85,47 +77,175 @@
*/
private final static RFC822DateFormat rfc822DateFormat = new RFC822DateFormat();
- private Socket socket; // The TCP/IP socket over which the SMTP
- // dialogue is occurring
+ /**
+ * The text string for the SMTP HELO command.
+ */
+ private final static String COMMAND_HELO = "HELO";
+
+ /**
+ * The text string for the SMTP EHLO command.
+ */
+ private final static String COMMAND_EHLO = "EHLO";
+
+ /**
+ * The text string for the SMTP AUTH command.
+ */
+ private final static String COMMAND_AUTH = "AUTH";
+
+ /**
+ * The text string for the SMTP MAIL command.
+ */
+ private final static String COMMAND_MAIL = "MAIL";
+
+ /**
+ * The text string for the SMTP RCPT command.
+ */
+ private final static String COMMAND_RCPT = "RCPT";
+
+ /**
+ * The text string for the SMTP NOOP command.
+ */
+ private final static String COMMAND_NOOP = "NOOP";
+
+ /**
+ * The text string for the SMTP RSET command.
+ */
+ private final static String COMMAND_RSET = "RSET";
+
+ /**
+ * The text string for the SMTP DATA command.
+ */
+ private final static String COMMAND_DATA = "DATA";
+
+ /**
+ * The text string for the SMTP QUIT command.
+ */
+ private final static String COMMAND_QUIT = "QUIT";
+
+ /**
+ * The text string for the SMTP HELP command.
+ */
+ private final static String COMMAND_HELP = "HELP";
+
+ /**
+ * The text string for the SMTP VRFY command.
+ */
+ private final static String COMMAND_VRFY = "VRFY";
+
+ /**
+ * The text string for the SMTP EXPN command.
+ */
+ private final static String COMMAND_EXPN = "EXPN";
+
+ /**
+ * The text string for the SMTP AUTH type PLAIN.
+ */
+ private final static String AUTH_TYPE_PLAIN = "PLAIN";
+
+ /**
+ * The text string for the SMTP AUTH type LOGIN.
+ */
+ private final static String AUTH_TYPE_LOGIN = "LOGIN";
+
+ /**
+ * The text string for the SMTP MAIL command SIZE option.
+ */
+ private final static String MAIL_OPTION_SIZE = "SIZE";
+
+
+ /**
+ * The TCP/IP socket over which the SMTP
+ * dialogue is occurring.
+ */
+ private Socket socket;
+
+ /**
+ * The incoming stream of bytes coming from the socket.
+ */
+ private InputStream in;
- private InputStream in; // The incoming stream of bytes coming from the socket.
- private PrintWriter out; // The writer to which outgoing messages are written.
+ /**
+ * The writer to which outgoing messages are written.
+ */
+ private PrintWriter out;
/**
* A Reader wrapper for the incoming stream of bytes coming from the socket.
*/
private BufferedReader inReader;
- private String remoteHost; // The remote host name obtained by lookup on the socket.
- private String remoteIP; // The remote IP address of the socket.
- private String smtpID; // The id associated with this particular SMTP interaction.
-
- private boolean authRequired = false; // Whether authentication is required to use
- // this SMTP server.
-
- private boolean verifyIdentity = false; // Whether the server verifies that the user
- // actually sending an email matches the
- // authentication credentials attached to the
- // SMTP interaction.
-
- private long maxmessagesize = 0; // The maximum message size allowed by this
- // SMTP server. The default value, 0, means
- // no limit.
-
- private int lengthReset = 20000; // The number of bytes to read before resetting
- // the connection timeout timer. Defaults to
- // 20 seconds.
+ /**
+ * The remote host name obtained by lookup on the socket.
+ */
+ private String remoteHost;
+
+ /**
+ * The remote IP address of the socket.
+ */
+ private String remoteIP;
+
+ /**
+ * The user name of the authenticated user associated with this SMTP transaction.
+ */
+ private String authenticatedUser;
+
+ /**
+ * The id associated with this particular SMTP interaction.
+ */
+ private String smtpID;
+
+ /**
+ * Whether authentication is required to use
+ * this SMTP server.
+ */
+ private boolean authRequired = false;
+
+
+ /**
+ * Whether the server verifies that the user
+ * actually sending an email matches the
+ * authentication credentials attached to the
+ * SMTP interaction.
+ */
+ private boolean verifyIdentity = false;
+
+
+ /**
+ * The maximum message size allowed by this SMTP server. The default
+ * value, 0, means no limit.
+ */
+ private long maxmessagesize = 0;
+
+ /**
+ * The number of bytes to read before resetting the connection timeout
+ * timer. Defaults to 20,000 bytes.
+ */
+ private int lengthReset = 20000;
- private TimeScheduler scheduler; // The scheduler used to handle timeouts for the SMTP
- // interaction
+ /**
+ * The scheduler used to handle timeouts for the SMTP interaction.
+ */
+ private TimeScheduler scheduler;
- private UsersRepository users; // The user repository for this server - used to authenticate
- // users.
+ /**
+ * The user repository for this server - used to authenticate
+ * users.
+ */
+ private UsersRepository users;
- private MailServer mailServer; // The internal mail server service
+ /**
+ * The internal mail server service.
+ */
+ private MailServer mailServer;
- private HashMap state = new HashMap(); // The hash map that holds variables for the SMTP
- // session in progress.
+ /**
+ * The hash map that holds variables for the SMTP message transfer in progress.
+ *
+ * This hash map should only be used to store variable set in a particular
+ * set of sequential MAIL-RCPT-DATA commands, as described in RFC 2821. Per
+ * connection values should be stored as member variables in this class.
+ */
+ private HashMap state = new HashMap();
/**
* Pass the <code>ComponentManager</code> to the <code>composer</code>.
@@ -214,7 +334,7 @@
try {
// Initially greet the connector
- // Format is: Sat, 24 Jan 1998 13:16:09 -0500
+ // Format is: Sat, 24 Jan 1998 13:16:09 -0500
final PeriodicTimeTrigger trigger = new PeriodicTimeTrigger( timeout, -1 );
scheduler.addTrigger( this.toString(), trigger, this );
@@ -227,11 +347,9 @@
.append(") ready ")
.append(rfc822DateFormat.format(new Date()));
String responseString = responseBuffer.toString();
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
+ writeLoggedFlushedResponse(responseString);
- while (parseCommand(inReader.readLine())) {
+ while (parseCommand(readCommandLine())) {
scheduler.resetTrigger(this.toString());
}
getLogger().debug("Closing socket.");
@@ -287,6 +405,8 @@
* being used as a timeout, so the method simply closes the connection.
*
* @param triggerName the name of the trigger
+ *
+ * TODO: Remove this interface from the class and change the mechanism
*/
public void targetTriggered(final String triggerName) {
getLogger().error("Connection timeout on socket");
@@ -294,26 +414,83 @@
out.println("Connection timeout. Closing connection");
socket.close();
} catch (IOException e) {
+ // Ignored
}
}
/**
+ * This method logs at a "DEBUG" level the response string that
+ * was sent to the SMTP client. The method is provided largely
+ * as syntactic sugar to neaten up the code base. It is declared
+ * private and final to encourage compiler inlining.
+ *
+ * @param responseString the response string sent to the client
+ */
+ private final void logResponseString(String responseString) {
+ if (getLogger().isDebugEnabled()) {
+ getLogger().debug("Sent: " + responseString);
+ }
+ }
+
+ /**
+ * Write and flush a response string. The response is also logged.
+ * Should be used for the last line of a multi-line response or
+ * for a single line response.
+ *
+ * @param responseString the response string sent to the client
+ */
+ final void writeLoggedFlushedResponse(String responseString) {
+ out.println(responseString);
+ out.flush();
+ logResponseString(responseString);
+ }
+
+ /**
+ * Write a response string. The response is also logged.
+ * Used for multi-line responses.
+ *
+ * @param responseString the response string sent to the client
+ */
+ final void writeLoggedResponse(String responseString) {
+ out.println(responseString);
+ out.flush();
+ logResponseString(responseString);
+ }
+
+ /**
+ * Reads a line of characters off the command line.
+ *
+ * @return the trimmed input line
+ * @throws IOException if an exception is generated reading in the input characters
+ */
+ final String readCommandLine() throws IOException {
+ return inReader.readLine().trim();
+ }
+
+ /**
+ * Sets the user name associated with this SMTP interaction.
+ *
+ * @param userID the user name
+ */
+ private void setUser(String userID) {
+ authenticatedUser = userID;
+ }
+
+ /**
+ * Returns the user name associated with this SMTP interaction.
+ *
+ * @return the user name
+ */
+ private String getUser() {
+ return authenticatedUser;
+ }
+
+ /**
* Resets message-specific, but not authenticated user, state.
*
*/
private void resetState() {
- String user = (String) state.get(AUTH);
state.clear();
- state.put(SERVER_NAME, this.helloName);
- state.put(SERVER_TYPE, SOFTWARE_TYPE);
- state.put(REMOTE_NAME, remoteHost);
- state.put(REMOTE_IP, remoteIP);
- state.put(SMTP_ID, smtpID);
- // seems that after authenticating an smtp client sends
- // a RSET, so we need to remember that they are authenticated
- if (user != null) {
- state.put(AUTH, user);
- }
}
/**
@@ -324,52 +501,56 @@
* to parse the raw command string to determine exactly which handler should
* be called. It returns true if expecting additional commands, false otherwise.
*
- * @param commandRaw the raw command string passed in over the socket
+ * @param rawCommand the raw command string passed in over the socket
*
* @return whether additional commands are expected.
*/
- private boolean parseCommand(String command) throws Exception {
+ private boolean parseCommand(String rawCommand) throws Exception {
String argument = null;
- String argument1 = null;
boolean returnValue = true;
+ String command = rawCommand;
- if (command == null) {
+ if (rawCommand == null) {
return false;
}
if ((state.get(MESG_FAILED) == null) && (getLogger().isDebugEnabled())) {
getLogger().debug("Command received: " + command);
}
- command = command.trim();
- if (command.indexOf(" ") > 0) {
- argument = command.substring(command.indexOf(" ") + 1);
- command = command.substring(0, command.indexOf(" "));
- if (argument.indexOf(":") > 0) {
- argument1 = argument.substring(argument.indexOf(":") + 1);
- argument = argument.substring(0, argument.indexOf(":"));
- }
+ int spaceIndex = command.indexOf(" ");
+ if (spaceIndex > 0) {
+ argument = command.substring(spaceIndex + 1);
+ command = command.substring(0, spaceIndex);
}
command = command.toUpperCase(Locale.US);
- if (command.equals("HELO")) {
- doHELO(command, argument, argument1);
- } else if (command.equals("EHLO")) {
- doEHLO(command, argument, argument1);
- } else if (command.equals("AUTH")) {
- doAUTH(command, argument, argument1);
- } else if (command.equals("MAIL")) {
- doMAIL(command, argument, argument1);
- } else if (command.equals("RCPT")) {
- doRCPT(command, argument, argument1);
- } else if (command.equals("NOOP")) {
- doNOOP(command, argument, argument1);
- } else if (command.equals("RSET")) {
- doRSET(command, argument, argument1);
- } else if (command.equals("DATA")) {
- doDATA(command, argument, argument1);
- } else if (command.equals("QUIT")) {
- doQUIT(command, argument, argument1);
+ if (command.equals(COMMAND_HELO)) {
+ doHELO(argument);
+ } else if (command.equals(COMMAND_EHLO)) {
+ doEHLO(argument);
+ } else if (command.equals(COMMAND_AUTH)) {
+ doAUTH(argument);
+ } else if (command.equals(COMMAND_MAIL)) {
+ doMAIL(argument);
+ } else if (command.equals(COMMAND_RCPT)) {
+ doRCPT(argument);
+ } else if (command.equals(COMMAND_NOOP)) {
+ doNOOP(argument);
+ } else if (command.equals(COMMAND_RSET)) {
+ doRSET(argument);
+ } else if (command.equals(COMMAND_DATA)) {
+ doDATA(argument);
+ } else if (command.equals(COMMAND_QUIT)) {
+ doQUIT(argument);
returnValue = false;
+ } else if (command.equals(COMMAND_VRFY)) {
+ doVRFY(argument);
+ } else if (command.equals(COMMAND_EXPN)) {
+ doEXPN(argument);
+ } else if (command.equals(COMMAND_HELP)) {
+ doHELP(argument);
} else {
- doUnknownCmd(command, argument, argument1);
+ if (state.get(MESG_FAILED) == null) {
+ doUnknownCmd(command, argument);
+ }
}
return returnValue;
}
@@ -379,26 +560,16 @@
* Responds with a greeting and informs the client whether
* client authentication is required.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doHELO(String command, String argument, String argument1) {
+ private void doHELO(String argument) {
String responseString = null;
- if (state.containsKey(CURRENT_HELO_MODE)) {
- StringBuffer responseBuffer =
- new StringBuffer(96)
- .append("250 ")
- .append(state.get(SERVER_NAME))
- .append(" Duplicate HELO");
- responseString = responseBuffer.toString();
- out.println(responseString);
- } else if (argument == null) {
- responseString = "501 domain address required: " + command;
- out.println(responseString);
+ if (argument == null) {
+ responseString = "501 Domain address required: " + COMMAND_HELO;
+ writeLoggedFlushedResponse(responseString);
} else {
- state.put(CURRENT_HELO_MODE, command);
- state.put(NAME_GIVEN, argument);
+ resetState();
+ state.put(CURRENT_HELO_MODE, COMMAND_HELO);
StringBuffer responseBuffer = new StringBuffer(256);
if (authRequired) {
//This is necessary because we're going to do a multiline response
@@ -406,13 +577,13 @@
} else {
responseBuffer.append("250 ");
}
- responseBuffer.append(state.get(SERVER_NAME))
+ responseBuffer.append(helloName)
.append(" Hello ")
.append(argument)
.append(" (")
- .append(state.get(REMOTE_NAME))
+ .append(remoteHost)
.append(" [")
- .append(state.get(REMOTE_IP))
+ .append(remoteIP)
.append("])");
responseString = responseBuffer.toString();
out.println(responseString);
@@ -431,31 +602,20 @@
* Responds with a greeting and informs the client whether
* client authentication is required.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doEHLO(String command, String argument, String argument1) {
+ private void doEHLO(String argument) {
String responseString = null;
- if (state.containsKey(CURRENT_HELO_MODE)) {
- StringBuffer responseBuffer =
- new StringBuffer(96)
- .append("250 ")
- .append(state.get(SERVER_NAME))
- .append(" Duplicate EHLO");
- responseString = responseBuffer.toString();
- out.println(responseString);
- } else if (argument == null) {
- responseString = "501 domain address required: " + command;
- out.println(responseString);
+ if (argument == null) {
+ responseString = "501 Domain address required: " + COMMAND_EHLO;
+ writeLoggedFlushedResponse(responseString);
} else {
- state.put(CURRENT_HELO_MODE, command);
- state.put(NAME_GIVEN, argument);
+ resetState();
+ state.put(CURRENT_HELO_MODE, COMMAND_EHLO);
// Extension defined in RFC 1870
if (maxmessagesize > 0) {
responseString = "250-SIZE " + maxmessagesize;
- out.println(responseString);
- logResponseString(responseString);
+ writeLoggedResponse(responseString);
}
StringBuffer responseBuffer = new StringBuffer(256);
if (authRequired) {
@@ -464,182 +624,204 @@
} else {
responseBuffer.append("250 ");
}
- responseBuffer.append(state.get(SERVER_NAME))
+ responseBuffer.append(helloName)
.append(" Hello ")
.append(argument)
.append(" (")
- .append(state.get(REMOTE_NAME))
+ .append(remoteHost)
.append(" [")
- .append(state.get(REMOTE_IP))
+ .append(remoteIP)
.append("])");
responseString = responseBuffer.toString();
- out.println(responseString);
if (authRequired) {
- logResponseString(responseString);
+ writeLoggedResponse(responseString);
responseString = "250 AUTH LOGIN PLAIN";
- out.println(responseString);
}
+ writeLoggedFlushedResponse(responseString);
}
- out.flush();
- logResponseString(responseString);
}
/**
* Handler method called upon receipt of a AUTH command.
* Handles client authentication to the SMTP server.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doAUTH(String command, String argument, String argument1)
+ private void doAUTH(String argument)
throws Exception {
String responseString = null;
- if (state.containsKey(AUTH)) {
+ if (getUser() != null) {
responseString = "503 User has previously authenticated. "
+ " Further authentication is not required!";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else if (argument == null) {
responseString = "501 Usage: AUTH (authentication type) <challenge>";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else {
- if ((argument1 == null) && (argument.indexOf(" ") > 0)) {
- argument1 = argument.substring(argument.indexOf(" ")+1);
+ String initialResponse = null;
+ if ((argument != null) && (argument.indexOf(" ") > 0)) {
+ initialResponse = argument.substring(argument.indexOf(" ") + 1);
argument = argument.substring(0,argument.indexOf(" "));
}
- argument = argument.toUpperCase(Locale.US);
- if (argument.equals("PLAIN")) {
- String userpass = null, user = null, pass = null;
- StringTokenizer authTokenizer;
- if (argument1 == null) {
- responseString = "334 OK. Continue authentication";
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
- userpass = inReader.readLine().trim();
- } else {
- userpass = argument1.trim();
- }
- try {
- if (userpass != null) {
- userpass = Base64.decodeAsString(userpass);
- }
- if (userpass != null) {
- authTokenizer = new StringTokenizer(userpass, "\0");
- user = authTokenizer.nextToken();
- pass = authTokenizer.nextToken();
- }
- }
- catch (Exception e) {
- // Ignored - this exception in parsing will be dealt
- // with in the if clause below
- }
- // Authenticate user
- if ((user == null) || (pass == null)) {
- responseString = "501 Could not decode parameters for AUTH PLAIN";
- out.println(responseString);
- out.flush();
- } else if (users.test(user, pass)) {
- state.put(AUTH, user);
- responseString = "235 Authentication Successful";
- out.println(responseString);
- getLogger().info("AUTH method PLAIN succeeded");
- } else {
- responseString = "535 Authentication Failed";
- out.println(responseString);
- getLogger().error("AUTH method PLAIN failed");
- }
- logResponseString(responseString);
+ String authType = argument.toUpperCase(Locale.US);
+ if (authType.equals(AUTH_TYPE_PLAIN)) {
+ doPlainAuth(initialResponse);
return;
- } else if (argument.equals("LOGIN")) {
- String user = null, pass = null;
- if (argument1 == null) {
- responseString = "334 VXNlcm5hbWU6"; // base64 encoded "Username:"
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
- user = inReader.readLine().trim();
- } else {
- user = argument1.trim();
- }
- if (user != null) {
- try {
- user = Base64.decodeAsString(user);
- } catch (Exception e) {
- // Ignored - this parse error will be
- // addressed in the if clause below
- user = null;
- }
- }
- responseString = "334 UGFzc3dvcmQ6"; // base64 encoded "Password:"
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
- pass = inReader.readLine().trim();
- if (pass != null) {
- try {
- pass = Base64.decodeAsString(pass);
- } catch (Exception e) {
- // Ignored - this parse error will be
- // addressed in the if clause below
- pass = null;
- }
- }
- // Authenticate user
- if ((user == null) || (pass == null)) {
- responseString = "501 Could not decode parameters for AUTH LOGIN";
- out.println(responseString);
- } else if (users.test(user, pass)) {
- state.put(AUTH, user);
- responseString = "235 Authentication Successful";
- out.println(responseString);
- getLogger().info("AUTH method LOGIN succeeded");
- } else {
- responseString = "535 Authentication Failed";
- out.println(responseString);
- getLogger().error("AUTH method LOGIN failed");
- }
- out.flush();
- logResponseString(responseString);
+ } else if (authType.equals(AUTH_TYPE_LOGIN)) {
+ doLoginAuth(initialResponse);
return;
} else {
- responseString = "504 Unrecognized Authentication Type";
- out.println(responseString);
- logResponseString(responseString);
- if (getLogger().isErrorEnabled()) {
- StringBuffer errorBuffer =
- new StringBuffer(128)
- .append("AUTH method ")
- .append(argument)
- .append(" is an unrecognized authentication type");
- getLogger().error(errorBuffer.toString());
- }
+ doUnknownAuth(authType, initialResponse);
return;
}
}
- out.flush();
- logResponseString(responseString);
+ }
+
+ /**
+ * Carries out the Plain AUTH SASL exchange.
+ *
+ * @param initialResponse the initial response line passed in with the AUTH command
+ */
+ private void doPlainAuth(String initialResponse)
+ throws IOException {
+ String userpass = null, user = null, pass = null, responseString = null;
+ if (initialResponse == null) {
+ responseString = "334 OK. Continue authentication";
+ writeLoggedFlushedResponse(responseString);
+ userpass = readCommandLine();
+ } else {
+ userpass = initialResponse.trim();
+ }
+ try {
+ if (userpass != null) {
+ userpass = Base64.decodeAsString(userpass);
+ }
+ if (userpass != null) {
+ StringTokenizer authTokenizer = new StringTokenizer(userpass, "\0");
+ user = authTokenizer.nextToken();
+ pass = authTokenizer.nextToken();
+ }
+ }
+ catch (Exception e) {
+ // Ignored - this exception in parsing will be dealt
+ // with in the if clause below
+ }
+ // Authenticate user
+ if ((user == null) || (pass == null)) {
+ responseString = "501 Could not decode parameters for AUTH PLAIN";
+ writeLoggedFlushedResponse(responseString);
+ } else if (users.test(user, pass)) {
+ setUser(user);
+ responseString = "235 Authentication Successful";
+ writeLoggedFlushedResponse(responseString);
+ getLogger().info("AUTH method PLAIN succeeded");
+ } else {
+ responseString = "535 Authentication Failed";
+ writeLoggedFlushedResponse(responseString);
+ getLogger().error("AUTH method PLAIN failed");
+ }
+ return;
+ }
+
+ /**
+ * Carries out the Login AUTH SASL exchange.
+ *
+ * @param initialResponse the initial response line passed in with the AUTH command
+ */
+ private void doLoginAuth(String initialResponse)
+ throws IOException {
+ String user = null, pass = null, responseString = null;
+ if (initialResponse == null) {
+ responseString = "334 VXNlcm5hbWU6"; // base64 encoded "Username:"
+ writeLoggedFlushedResponse(responseString);
+ user = readCommandLine();
+ } else {
+ user = initialResponse.trim();
+ }
+ if (user != null) {
+ try {
+ user = Base64.decodeAsString(user);
+ } catch (Exception e) {
+ // Ignored - this parse error will be
+ // addressed in the if clause below
+ user = null;
+ }
+ }
+ responseString = "334 UGFzc3dvcmQ6"; // base64 encoded "Password:"
+ writeLoggedFlushedResponse(responseString);
+ pass = readCommandLine();
+ if (pass != null) {
+ try {
+ pass = Base64.decodeAsString(pass);
+ } catch (Exception e) {
+ // Ignored - this parse error will be
+ // addressed in the if clause below
+ pass = null;
+ }
+ }
+ // Authenticate user
+ if ((user == null) || (pass == null)) {
+ responseString = "501 Could not decode parameters for AUTH LOGIN";
+ } else if (users.test(user, pass)) {
+ setUser(user);
+ responseString = "235 Authentication Successful";
+ if (getLogger().isDebugEnabled()) {
+ // TODO: Make this string a more useful debug message
+ getLogger().debug("AUTH method LOGIN succeeded");
+ }
+ } else {
+ responseString = "535 Authentication Failed";
+ // TODO: Make this string a more useful error message
+ getLogger().error("AUTH method LOGIN failed");
+ }
+ writeLoggedFlushedResponse(responseString);
+ return;
+ }
+
+ /**
+ * Handles the case of an unrecognized auth type.
+ *
+ * @param authType the unknown auth type
+ * @param initialResponse the initial response line passed in with the AUTH command
+ */
+ private void doUnknownAuth(String authType, String initialResponse) {
+ String responseString = "504 Unrecognized Authentication Type";
+ writeLoggedFlushedResponse(responseString);
+ if (getLogger().isErrorEnabled()) {
+ StringBuffer errorBuffer =
+ new StringBuffer(128)
+ .append("AUTH method ")
+ .append(authType)
+ .append(" is an unrecognized authentication type");
+ getLogger().error(errorBuffer.toString());
+ }
+ return;
}
/**
* Handler method called upon receipt of a MAIL command.
* Sets up handler to deliver mail as the stated sender.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doMAIL(String command, String argument, String argument1) {
+ private void doMAIL(String argument) {
String responseString = null;
+
+ String sender = null;
+ if ((argument != null) && (argument.indexOf(":") > 0)) {
+ int colonIndex = argument.indexOf(":");
+ sender = argument.substring(colonIndex + 1);
+ argument = argument.substring(0, colonIndex);
+ }
if (state.containsKey(SENDER)) {
responseString = "503 Sender already specified";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else if (argument == null || !argument.toUpperCase(Locale.US).equals("FROM")
- || argument1 == null) {
+ || sender == null) {
responseString = "501 Usage: MAIL FROM:<sender>";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else {
- String sender = argument1.trim();
+ sender = sender.trim();
int lastChar = sender.lastIndexOf('>');
// Check to see if any options are present and, if so, whether they are correctly formatted
// (separated from the closing angle bracket by a ' ').
@@ -662,37 +844,9 @@
// Handle the SIZE extension keyword
- // TODO: Encapsulate option logic in a method
- if (mailOptionName.startsWith("SIZE")) {
- int size = 0;
- try {
- size = Integer.parseInt(mailOptionValue);
- } catch (NumberFormatException pe) {
- // This is a malformed option value. We ignore it
- // and proceed to the next option.
- continue;
- }
- if (getLogger().isDebugEnabled()) {
- StringBuffer debugBuffer =
- new StringBuffer(128)
- .append("MAIL command option SIZE received with value ")
- .append(size)
- .append(".");
- getLogger().debug(debugBuffer.toString());
- }
- if ((maxmessagesize > 0) && (size > maxmessagesize)) {
- // Let the client know that the size limit has been hit.
- responseString = "552 Message size exceeds fixed maximum message size";
- out.println(responseString);
- out.flush();
-
- logResponseString(responseString);
- getLogger().error(responseString);
+ if (mailOptionName.startsWith(MAIL_OPTION_SIZE)) {
+ if (!(doMailSize(mailOptionValue))) {
return;
- } else {
- // put the message size in the message state so it can be used
- // later to restrict messages for user quotas, etc.
- state.put(MESG_SIZE, new Integer(size));
}
} else {
// Unexpected option attached to the Mail command
@@ -709,9 +863,8 @@
}
}
if (!sender.startsWith("<") || !sender.endsWith(">")) {
- responseString = "501 Syntax error in parameters or arguments";
- out.println(responseString);
- logResponseString(responseString);
+ responseString = "501 Syntax error in MAIL command";
+ writeLoggedFlushedResponse(responseString);
if (getLogger().isErrorEnabled()) {
StringBuffer errorBuffer =
new StringBuffer(128)
@@ -734,9 +887,8 @@
try {
senderAddress = new MailAddress(sender);
} catch (Exception pe) {
- responseString = "501 Syntax error in parameters or arguments";
- out.println(responseString);
- logResponseString(responseString);
+ responseString = "501 Syntax error in sender address";
+ writeLoggedFlushedResponse(responseString);
if (getLogger().isErrorEnabled()) {
StringBuffer errorBuffer =
new StringBuffer(256)
@@ -756,35 +908,89 @@
.append(sender)
.append("> OK");
responseString = responseBuffer.toString();
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
}
- out.flush();
- logResponseString(responseString);
}
/**
+ * Handles the SIZE MAIL option.
+ *
+ * @param mailOptionValue the option string passed in with the SIZE option
+ * @returns true if further options should be processed, false otherwise
+ */
+ private boolean doMailSize(String mailOptionValue) {
+ int size = 0;
+ try {
+ size = Integer.parseInt(mailOptionValue);
+ } catch (NumberFormatException pe) {
+ // This is a malformed option value. We return an error
+ String responseString = "501 Syntactically incorrect value for SIZE parameter";
+ writeLoggedFlushedResponse(responseString);
+ getLogger().error("Rejected syntactically incorrect value for SIZE parameter.");
+ return true;
+ }
+ if (getLogger().isDebugEnabled()) {
+ StringBuffer debugBuffer =
+ new StringBuffer(128)
+ .append("MAIL command option SIZE received with value ")
+ .append(size)
+ .append(".");
+ getLogger().debug(debugBuffer.toString());
+ }
+ if ((maxmessagesize > 0) && (size > maxmessagesize)) {
+ // Let the client know that the size limit has been hit.
+ String responseString = "552 Message size exceeds fixed maximum message size";
+ writeLoggedFlushedResponse(responseString);
+ StringBuffer errorBuffer =
+ new StringBuffer(256)
+ .append("Rejected message from ")
+ .append(state.get(SENDER).toString())
+ .append(" from host ")
+ .append(remoteHost)
+ .append(" of size ")
+ .append(size)
+ .append(" exceeding system maximum message size of ")
+ .append(maxmessagesize)
+ .append("based on SIZE option.");
+ getLogger().error(errorBuffer.toString());
+ return true;
+ } else {
+ // put the message size in the message state so it can be used
+ // later to restrict messages for user quotas, etc.
+ state.put(MESG_SIZE, new Integer(size));
+ }
+ return false;
+ }
+
+
+ /**
* Handler method called upon receipt of a RCPT command.
* Reads recipient. Does some connection validation.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doRCPT(String command, String argument, String argument1) {
+ private void doRCPT(String argument) {
String responseString = null;
+
+ String recipient = null;
+ if ((argument != null) && (argument.indexOf(":") > 0)) {
+ int colonIndex = argument.indexOf(":");
+ recipient = argument.substring(colonIndex + 1);
+ argument = argument.substring(0, colonIndex);
+ }
if (!state.containsKey(SENDER)) {
responseString = "503 Need MAIL before RCPT";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else if (argument == null || !argument.toUpperCase(Locale.US).equals("TO")
- || argument1 == null) {
+ || recipient == null) {
responseString = "501 Usage: RCPT TO:<recipient>";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else {
Collection rcptColl = (Collection) state.get(RCPT_VECTOR);
if (rcptColl == null) {
rcptColl = new Vector();
}
- String recipient = argument1.trim();
+ recipient = recipient.trim();
int lastChar = recipient.lastIndexOf('>');
// Check to see if any options are present and, if so, whether they are correctly formatted
// (separated from the closing angle bracket by a ' ').
@@ -818,9 +1024,7 @@
}
if (!recipient.startsWith("<") || !recipient.endsWith(">")) {
responseString = "501 Syntax error in parameters or arguments";
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
+ writeLoggedFlushedResponse(responseString);
if (getLogger().isErrorEnabled()) {
StringBuffer errorBuffer =
new StringBuffer(192)
@@ -840,9 +1044,8 @@
try {
recipientAddress = new MailAddress(recipient);
} catch (Exception pe) {
- responseString = "501 Syntax error in parameters or arguments";
- out.println(responseString);
- logResponseString(responseString);
+ responseString = "501 Syntax error in recipient address";
+ writeLoggedFlushedResponse(responseString);
if (getLogger().isErrorEnabled()) {
StringBuffer errorBuffer =
@@ -858,42 +1061,25 @@
if (authRequired) {
// Make sure the mail is being sent locally if not
// authenticated else reject.
- if (!state.containsKey(AUTH)) {
+ if (getUser() == null) {
String toDomain = recipientAddress.getHost();
if (!mailServer.isLocalServer(toDomain)) {
responseString = "530 Authentication Required";
- out.println(responseString);
- logResponseString(responseString);
- getLogger().error("Authentication is required for mail request");
+ writeLoggedFlushedResponse(responseString);
+ getLogger().error("Rejected message - authentication is required for mail request");
return;
}
} else {
// Identity verification checking
if (verifyIdentity) {
- String authUser = ((String) state.get(AUTH)).toLowerCase(Locale.US);
+ String authUser = (getUser()).toLowerCase(Locale.US);
MailAddress senderAddress = (MailAddress) state.get(SENDER);
boolean domainExists = false;
- if (!authUser.equals(senderAddress.getUser())) {
+ if ((!authUser.equals(senderAddress.getUser())) ||
+ (!mailServer.isLocalServer(senderAddress.getHost()))) {
responseString = "503 Incorrect Authentication for Specified Email Address";
- out.println(responseString);
- logResponseString(responseString);
- if (getLogger().isErrorEnabled()) {
- StringBuffer errorBuffer =
- new StringBuffer(128)
- .append("User ")
- .append(authUser)
- .append(" authenticated, however tried sending email as ")
- .append(senderAddress);
- getLogger().error(errorBuffer.toString());
- }
- return;
- }
- if (!mailServer.isLocalServer(
- senderAddress.getHost())) {
- responseString = "503 Incorrect Authentication for Specified Email Address";
- out.println(responseString);
- logResponseString(responseString);
+ writeLoggedFlushedResponse(responseString);
if (getLogger().isErrorEnabled()) {
StringBuffer errorBuffer =
new StringBuffer(128)
@@ -916,40 +1102,36 @@
.append(recipient)
.append("> OK");
responseString = responseBuffer.toString();
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
}
- out.flush();
- logResponseString(responseString);
}
/**
* Handler method called upon receipt of a NOOP command.
* Just sends back an OK and logs the command.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doNOOP(String command, String argument, String argument1) {
+ private void doNOOP(String argument) {
String responseString = "250 OK";
- out.println(responseString);
- logResponseString(responseString);
+ writeLoggedFlushedResponse(responseString);
}
/**
* Handler method called upon receipt of a RSET command.
* Resets message-specific, but not authenticated user, state.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doRSET(String command, String argument, String argument1) {
- resetState();
- String responseString = "250 OK";
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
+ private void doRSET(String argument) {
+ String responseString = "";
+ if ((argument == null) || (argument.length() == 0)) {
+ resetState();
+ responseString = "250 OK";
+ } else {
+ responseString = "500 Unexpected argument provided with RSET command";
+ }
+ writeLoggedFlushedResponse(responseString);
}
/**
@@ -957,23 +1139,23 @@
* Reads in message data, creates header, and delivers to
* mail server service for delivery.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doDATA(String command, String argument, String argument1) {
+ private void doDATA(String argument) {
String responseString = null;
+ if ((argument != null) && (argument.length() > 0)) {
+ responseString = "500 Unexpected argument provided with DATA command";
+ writeLoggedFlushedResponse(responseString);
+ }
if (!state.containsKey(SENDER)) {
responseString = "503 No sender specified";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else if (!state.containsKey(RCPT_VECTOR)) {
responseString = "503 No recipients specified";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
} else {
responseString = "354 Ok Send data ending with <CRLF>.<CRLF>";
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
+ writeLoggedFlushedResponse(responseString);
try {
// Setup the input stream to notify the scheduler periodically
InputStream msgIn =
@@ -993,11 +1175,11 @@
}
msgIn = new SizeLimitedInputStream(msgIn, maxmessagesize);
}
- //Removes the dot stuffing
+ // Removes the dot stuffing
msgIn = new SMTPInputStream(msgIn);
- //Parse out the message headers
+ // Parse out the message headers
MailHeaders headers = new MailHeaders(msgIn);
- // if headers do not contains minimum REQUIRED headers fields,
+ // If headers do not contains minimum REQUIRED headers fields,
// add them
if (!headers.isSet(RFC2822Headers.DATE)) {
headers.setHeader(RFC2822Headers.DATE, rfc822DateFormat.format(new Date()));
@@ -1005,7 +1187,7 @@
if (!headers.isSet(RFC2822Headers.FROM) && state.get(SENDER) != null) {
headers.setHeader(RFC2822Headers.FROM, state.get(SENDER).toString());
}
- //Determine the Return-Path
+ // Determine the Return-Path
String returnPath = headers.getHeader(RFC2822Headers.RETURN_PATH, "\r\n");
headers.removeHeader(RFC2822Headers.RETURN_PATH);
if (returnPath == null) {
@@ -1020,19 +1202,19 @@
returnPath = returnPathBuffer.toString();
}
}
- //We will rebuild the header object to put Return-Path and our
- // Received message at the top
+ // We will rebuild the header object to put Return-Path and our
+ // Received header at the top
Enumeration headerLines = headers.getAllHeaderLines();
headers = new MailHeaders();
- //Put the Return-Path first
+ // Put the Return-Path first
headers.addHeaderLine(RFC2822Headers.RETURN_PATH + ": " + returnPath);
- //Put our Received header next
+ // Put our Received header next
StringBuffer headerLineBuffer =
new StringBuffer(128)
.append(RFC2822Headers.RECEIVED + ": from ")
- .append(state.get(REMOTE_NAME))
+ .append(remoteHost)
.append(" ([")
- .append(state.get(REMOTE_IP))
+ .append(remoteIP)
.append("])");
headers.addHeaderLine(headerLineBuffer.toString());
@@ -1044,12 +1226,12 @@
.append(" (")
.append(SOFTWARE_TYPE)
.append(") with SMTP ID ")
- .append(state.get(SMTP_ID));
+ .append(smtpID);
if (((Collection) state.get(RCPT_VECTOR)).size() == 1) {
- //Only indicate a recipient if they're the only recipient
- //(prevents email address harvesting and large headers in
- // bulk email)
+ // Only indicate a recipient if they're the only recipient
+ // (prevents email address harvesting and large headers in
+ // bulk email)
headers.addHeaderLine(headerLineBuffer.toString());
headerLineBuffer =
new StringBuffer(256)
@@ -1058,13 +1240,13 @@
.append(">;");
headers.addHeaderLine(headerLineBuffer.toString());
} else {
- //Put the ; on the end of the 'by' line
+ // Put the ; on the end of the 'by' line
headerLineBuffer.append(";");
headers.addHeaderLine(headerLineBuffer.toString());
}
headers.addHeaderLine(" " + rfc822DateFormat.format(new Date()));
- //Add all the original message headers back in next
+ // Add all the original message headers back in next
while (headerLines.hasMoreElements()) {
headers.addHeaderLine((String) headerLines.nextElement());
}
@@ -1075,104 +1257,133 @@
(MailAddress) state.get(SENDER),
(Vector) state.get(RCPT_VECTOR),
new SequenceInputStream(headersIn, msgIn));
- // if the message size limit has been set, we'll
+ // If the message size limit has been set, we'll
// call mail.getSize() to force the message to be
// loaded. Need to do this to enforce the size limit
if (maxmessagesize > 0) {
mail.getMessageSize();
}
- mail.setRemoteHost((String) state.get(REMOTE_NAME));
- mail.setRemoteAddr((String) state.get(REMOTE_IP));
+ mail.setRemoteHost(remoteHost);
+ mail.setRemoteAddr(remoteIP);
mailServer.sendMail(mail);
+ if (getLogger().isDebugEnabled()) {
+ getLogger().debug("Successfully sent mail to spool: " + mail.getName());
+ }
} catch (MessagingException me) {
- //Grab any exception attached to this one.
+ // Grab any exception attached to this one.
Exception e = me.getNextException();
- //If there was an attached exception, and it's a
- //MessageSizeException
+ // If there was an attached exception, and it's a
+ // MessageSizeException
if (e != null && e instanceof MessageSizeException) {
// Add an item to the state to suppress
// logging of extra lines of data
// that are sent after the size limit has
// been hit.
state.put(MESG_FAILED, Boolean.TRUE);
- //then let the client know that the size
- //limit has been hit.
+ // then let the client know that the size
+ // limit has been hit.
responseString = "552 Error processing message: "
+ e.getMessage();
+ StringBuffer errorBuffer =
+ new StringBuffer(256)
+ .append("Rejected message from ")
+ .append(state.get(SENDER).toString())
+ .append(" from host ")
+ .append(remoteHost)
+ .append(" exceeding system maximum message size of ")
+ .append(maxmessagesize);
+ getLogger().error(errorBuffer.toString());
} else {
responseString = "451 Error processing message: "
+ me.getMessage();
+ getLogger().error("Unknown error occurred while processing DATA.", me);
}
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
- getLogger().error(responseString);
+ writeLoggedFlushedResponse(responseString);
return;
}
- getLogger().info("Mail sent to Mail Server");
resetState();
responseString = "250 Message received";
- out.println(responseString);
+ writeLoggedFlushedResponse(responseString);
}
- out.flush();
- logResponseString(responseString);
}
/**
- * Handler method called upon receipt of a QUIT command.
- * This method informs the client that the connection is
- * closing.
+ * Handler method called upon receipt of a VRFY command.
+ * This method informs the client that the command is
+ * not implemented.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doQUIT(String command, String argument, String argument1) {
- StringBuffer responseBuffer =
- new StringBuffer(128)
- .append("221 ")
- .append(state.get(SERVER_NAME))
- .append(" Service closing transmission channel");
- String responseString = responseBuffer.toString();
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
+ private void doVRFY(String argument) {
+
+ String responseString = "502 VRFY is not supported";
+ writeLoggedFlushedResponse(responseString);
}
/**
- * Handler method called upon receipt of an unrecognized command.
- * Returns an error response and logs the command.
+ * Handler method called upon receipt of a EXPN command.
+ * This method informs the client that the command is
+ * not implemented.
+ *
+ * @param argument the argument passed in with the command by the SMTP client
+ */
+ private void doEXPN(String argument) {
+
+ String responseString = "502 EXPN is not supported";
+ writeLoggedFlushedResponse(responseString);
+ }
+
+ /**
+ * Handler method called upon receipt of a HELP command.
+ * This method informs the client that the command is
+ * not implemented.
+ *
+ * @param argument the argument passed in with the command by the SMTP client
+ */
+ private void doHELP(String argument) {
+
+ String responseString = "502 HELP is not supported";
+ writeLoggedFlushedResponse(responseString);
+ }
+
+ /**
+ * Handler method called upon receipt of a QUIT command.
+ * This method informs the client that the connection is
+ * closing.
*
- * @param command the command parsed by the parseCommand method
- * @argument the first argument parsed by the parseCommand method
- * @argument1 the second argument parsed by the parseCommand method
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private void doUnknownCmd(String command, String argument, String argument1) {
- if (state.get(MESG_FAILED) == null) {
+ private void doQUIT(String argument) {
+
+ String responseString = "";
+ if ((argument == null) || (argument.length() == 0)) {
StringBuffer responseBuffer =
new StringBuffer(128)
- .append("500 ")
- .append(state.get(SERVER_NAME))
- .append(" Syntax error, command unrecognized: ")
- .append(command);
- String responseString = responseBuffer.toString();
- out.println(responseString);
- out.flush();
- logResponseString(responseString);
+ .append("221 ")
+ .append(helloName)
+ .append(" Service closing transmission channel");
+ responseString = responseBuffer.toString();
+ } else {
+ responseString = "500 Unexpected argument provided with QUIT command";
}
+ writeLoggedFlushedResponse(responseString);
}
/**
- * This method logs at a "DEBUG" level the response string that
- * was sent to the SMTP client. The method is provided largely
- * as syntactic sugar to neaten up the code base. It is declared
- * private and final to encourage compiler inlining.
+ * Handler method called upon receipt of an unrecognized command.
+ * Returns an error response and logs the command.
*
- * @param responseString the response string sent to the client
+ * @param command the command parsed by the SMTP client
+ * @param argument the argument passed in with the command by the SMTP client
*/
- private final void logResponseString(String responseString) {
- if (getLogger().isDebugEnabled()) {
- getLogger().debug("Sent: " + responseString);
- }
+ private void doUnknownCmd(String command, String argument) {
+ StringBuffer responseBuffer =
+ new StringBuffer(128)
+ .append("500 ")
+ .append(helloName)
+ .append(" Syntax error, command unrecognized: ")
+ .append(command);
+ String responseString = responseBuffer.toString();
+ writeLoggedFlushedResponse(responseString);
}
}
--
To unsubscribe, e-mail: <ma...@jakarta.apache.org>
For additional commands, e-mail: <ma...@jakarta.apache.org>