package mireka.pop;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.smtp.io.CRLFTerminatedReader;
/**
* SessionThread manages the TCP connection to the POP3 client and contains the
* loop which processes the incoming commands.
*/
public class SessionThread extends Thread {
private static final int TEN_MINUTES = 10 * 60 * 1000;
private final Logger log = LoggerFactory.getLogger(SessionThread.class);
private final ServerThread serverThread;
private final CommandHandler commandHandler;
/** I/O to the client */
private Socket socket;
private InputStream input;
/**
* Remark: POP3 command limit is 255 octets according to RFC 2449 #4
*/
private CRLFTerminatedReader reader;
private Writer writer;
/** Set this true when doing an ordered shutdown */
private volatile boolean quitting = false;
private Session session;
public SessionThread(PopServer server, ServerThread serverThread,
Socket socket) throws IOException {
super(SessionThread.class.getName() + "-" + socket.getInetAddress()
+ ":" + socket.getPort());
this.serverThread = serverThread;
setSocket(socket);
session = new Session(server, this);
this.commandHandler = new CommandHandler(session);
}
@Override
public void run() {
try {
doRun();
} finally {
serverThread.sessionEnded(this);
}
}
private void doRun() {
if (log.isDebugEnabled()) {
InetAddress remoteInetAddress =
this.getRemoteAddress().getAddress();
remoteInetAddress.getHostName(); // Causes future toString() to
// print the name too
log.debug("POP3 connection from {}, new connection count: {}",
remoteInetAddress, serverThread.getNumberOfConnections());
}
try {
if (serverThread.hasTooManyConnections()) {
log.debug("POP3 Too many connections!");
this.sendResponse("-ERR [SYS/TEMP] Too many connections, try again later");
return;
}
commandHandler.sendWelcomeMessage();
while (!this.quitting) {
try {
String line = null;
try {
line = this.reader.readLine();
} catch (SocketException ex) {
// Lots of clients just "hang up" rather than issuing
// QUIT, which would
// fill our logs with the warning in the outer catch.
if (log.isDebugEnabled())
log.debug(
"Error reading client command: "
+ ex.getMessage(), ex);
return;
}
if (line == null) {
log.debug("no more lines from client");
return;
}
logClientLineSecurely(line);
commandHandler.handleCommand(line);
} catch (SocketTimeoutException ex) {
// according to RFC 1939 no response should be sent on
// timeout
log.debug("Socket timeout: " + ex.getMessage());
return;
} catch (CRLFTerminatedReader.TerminationException te) {
String msg =
"-ERR Syntax error at character position "
+ te.position()
+ ". CR and LF must be CRLF paired. See RFC 1939 #3";
log.debug(msg);
this.sendResponse(msg);
// if people are screwing with things, close connection
return;
} catch (CRLFTerminatedReader.MaxLineLengthException mlle) {
String msg = "-ERR " + mlle.getMessage();
log.debug(msg);
this.sendResponse(msg);
// if people are screwing with things, close connection
return;
}
}
} catch (IOException e1) {
if (!this.quitting) {
try {
// Send a temporary failure back so that the server will try
// to resend
// the message later.
this.sendResponse("-ERR [SYS/TEMP] Problem attempting to execute commands. Please try again later.");
} catch (IOException e) {
// it is expected that a response for an IO error cannot be
// sent
}
if (log.isWarnEnabled())
log.warn("Exception during POP session", e1);
}
} finally {
this.closeConnection();
this.notifyCommandHandlerOnDisconnect();
}
}
/** Sends the response to the client */
public void sendResponse(String response) throws IOException {
if (log.isDebugEnabled())
log.debug("Server: " + response);
this.writer.write(response + "\r\n");
this.writer.flush();
}
/**
* It logs the line but masks out any clear text passwords
*/
private void logClientLineSecurely(String line) {
if (!log.isDebugEnabled())
return;
if (line.toUpperCase(Locale.US).startsWith("PASS ")) {
line = line.substring(0, 5) + "*****";
}
log.debug("Client: " + line);
}
/**
* Close reader, writer, and socket, logging exceptions but otherwise
* ignoring them
*/
private void closeConnection() {
try {
try {
this.writer.close();
this.input.close();
} finally {
this.closeSocket();
}
} catch (IOException e) {
log.info(e.toString());
}
}
/** Close the client socket if it is open */
private void closeSocket() throws IOException {
if ((this.socket != null) && this.socket.isBound()
&& !this.socket.isClosed())
this.socket.close();
}
/** Safely calls connectionClosed() on the command handler */
private void notifyCommandHandlerOnDisconnect() {
try {
commandHandler.connectionClosed();
} catch (Exception ex) {
log.error("Exception in command handler", ex);
}
}
public OutputStream getOutputStream() throws IOException {
writer.flush();
return socket.getOutputStream();
}
public void shutdown() {
quit();
}
/**
* Triggers the shutdown of the thread and the closing of the connection.
*/
public void quit() {
quitting = true;
closeConnection();
}
/**
* Returns the current socket. This function is called when the original
* socket is to be wrapped by an SSLSocket, after the STLS command is
* received.
*/
public Socket getSocket() {
return socket;
}
/**
* Initializes our reader, writer, and the i/o filter chains based on the
* specified socket. This is called internally when we startup and when (if)
* SSL is started.
*/
public void setSocket(Socket socket) throws IOException {
this.socket = socket;
this.input = this.socket.getInputStream();
this.reader = new CRLFTerminatedReader(this.input);
this.writer =
new OutputStreamWriter(this.socket.getOutputStream(),
"US-ASCII");
this.socket.setSoTimeout(TEN_MINUTES);
}
private InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) this.socket.getRemoteSocketAddress();
}
}