package org.subethamail.smtp.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.cert.Certificate;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.subethamail.smtp.AuthenticationHandler;
import org.subethamail.smtp.DropConnectionException;
import org.subethamail.smtp.MessageContext;
import org.subethamail.smtp.MessageHandler;
import org.subethamail.smtp.io.CRLFTerminatedReader;

/**
 * The thread that handles a connection. This class
 * passes most of it's responsibilities off to the
 * CommandHandler.
 *
 * @author Jon Stevens
 * @author Jeff Schnitzer
 */
public class Session implements Runnable, MessageContext
{
	private final static Logger log = LoggerFactory.getLogger(Session.class);

	/** A link to our parent server */
	private final SMTPServer server;

	/**
	 * A link to our parent server thread, which must be notified when this
	 * connection is finished.
	 */
	private final ServerThread serverThread;
	
	/**
	 * Saved SLF4J mapped diagnostic context of the parent thread. The parent
	 * thread is the one which calls the constructor. MDC is usually inherited
	 * by new threads, but this mechanism does not work with executors.
	 */
	private final Map<?, ?> parentLoggingMdcContext = MDC.getCopyOfContextMap();
	
	/**
	 * Uniquely identifies this session within an extended time period, useful
	 * for logging.
	 */
	private String sessionId;

	/** Set this true when doing an ordered shutdown */
	private volatile boolean quitting = false;

	/** I/O to the client */
	private Socket socket;
	private InputStream input;
	private CRLFTerminatedReader reader;
	private PrintWriter writer;

	/** Might exist if the client has successfully authenticated */
	private AuthenticationHandler authenticationHandler;

	/**
	 * It exists if a mail transaction is in progress (from the MAIL command
	 * up to the end of the DATA command).
	 */
	private MessageHandler messageHandler;

	/** Some state information */
	private String helo;
	private int recipientCount;
	/**
	 * The recipient address in the first accepted RCPT command, but only if
	 * there is exactly one such accepted recipient. If there is no accepted
	 * recipient yet, or if there are more than one, then this value is null.
	 * This information is useful in the construction of the FOR clause of the
	 * Received header.
	 */
	private String singleRecipient;

	/**
	 * If the client told us the size of the message, this is the value.
	 * If they didn't, the value will be 0.
	 */
	private int declaredMessageSize = 0;

	/** Some more state information */
	private boolean tlsStarted;
	private Certificate[] tlsPeerCertificates;

	/**
	 * Creates the Runnable Session object.
	 *
	 * @param server a link to our parent
	 * @param socket is the socket to the client
	 * @throws IOException
	 */
	public Session(SMTPServer server, ServerThread serverThread, Socket socket)
		throws IOException
	{
		this.server = server;
		this.serverThread = serverThread;

		this.setSocket(socket);
	}

	/**
	 * @return a reference to the master server object
	 */
	public SMTPServer getServer()
	{
		return this.server;
	}

	/**
	 * The thread for each session runs on this and shuts down when the quitting
	 * member goes true.
	 */
	@Override
	public void run()
	{
		MDC.setContextMap(parentLoggingMdcContext);
		sessionId = server.getSessionIdFactory().create();
		MDC.put("SessionId", sessionId);
		final String originalName = Thread.currentThread().getName();
		Thread.currentThread().setName(
				Session.class.getName() + "-" + socket.getInetAddress() + ":"
						+ socket.getPort());

		if (log.isDebugEnabled())
		{
			InetAddress remoteInetAddress = this.getRemoteAddress().getAddress();
			remoteInetAddress.getHostName();	// Causes future toString() to print the name too

			log.debug("SMTP connection from {}, new connection count: {}", remoteInetAddress,
					this.serverThread.getNumberOfConnections());
		}

		try
		{
			runCommandLoop();
		}
		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("421 4.4.0 Problem attempting to execute commands. Please try again later.");
				}
				catch (IOException e) {}

				if (log.isWarnEnabled())
					log.warn("Exception during SMTP transaction", e1);
			}
		}
		catch (Throwable e)
		{
			log.error("Unexpected error in the SMTP handler thread", e);
			try
			{
				this.sendResponse("421 4.3.0 Mail system failure, closing transmission channel");
			}
			catch (IOException e1)
			{
				// just swallow this, the outer exception is the real problem.
			}
			if (e instanceof RuntimeException)
				throw (RuntimeException) e;
			else if (e instanceof Error)
				throw (Error) e;
			else
				throw new RuntimeException("Unexpected exception", e);
		}
		finally
		{
			this.closeConnection();
			this.endMessageHandler();
			serverThread.sessionEnded(this);
			Thread.currentThread().setName(originalName);
			MDC.clear();
		}
	}

	/**
	 * Sends the welcome message and starts receiving and processing client
	 * commands. It quits when {@link #quitting} becomes true or when it can be
	 * noticed or at least assumed that the client no longer sends valid
	 * commands, for example on timeout.
	 * 
	 * @throws IOException
	 *             if sending to or receiving from the client fails.
	 */
	private void runCommandLoop() throws IOException
	{
		if (this.serverThread.hasTooManyConnections())
		{
			log.debug("SMTP Too many connections!");

			this.sendResponse("421 Too many connections, try again later");
			return;
		}

		this.sendResponse("220 " + this.server.getHostName() + " ESMTP " + this.server.getSoftwareName());

		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;
				}

				if (log.isDebugEnabled())
					log.debug("Client: " + line);

				this.server.getCommandHandler().handleCommand(this, line);
			}
			catch (DropConnectionException ex)
			{
				this.sendResponse(ex.getErrorResponse());
				return;
			}
			catch (SocketTimeoutException ex)
			{
				this.sendResponse("421 Timeout waiting for data from client.");
				return;
			}
			catch (CRLFTerminatedReader.TerminationException te)
			{
				String msg = "501 Syntax error at character position " + te.position()
						+ ". CR and LF must be CRLF paired.  See RFC 2821 #2.7.1.";

				log.debug(msg);
				this.sendResponse(msg);

				// if people are screwing with things, close connection
				return;
			}
			catch (CRLFTerminatedReader.MaxLineLengthException mlle)
			{
				String msg = "501 " + mlle.getMessage();

				log.debug(msg);
				this.sendResponse(msg);

				// if people are screwing with things, close connection
				return;
			}
		}
	}

	/**
	 * 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());
		}
	}

	/**
	 * 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 PrintWriter(this.socket.getOutputStream());

		this.socket.setSoTimeout(this.server.getConnectionTimeout());
	}

	/**
	 * This method is only used by the start tls command
	 * @return the current socket to the client
	 */
	public Socket getSocket()
	{
		return this.socket;
	}

	/** Close the client socket if it is open */
	public void closeSocket() throws IOException
	{
		if ((this.socket != null) && this.socket.isBound() && !this.socket.isClosed())
			this.socket.close();
	}

	/**
	 * @return the raw input stream from the client
	 */
	public InputStream getRawInput()
	{
		return this.input;
	}

	/**
	 * @return the cooked CRLF-terminated reader from the client
	 */
	public CRLFTerminatedReader getReader()
	{
		return this.reader;
	}

	/** Sends the response to the client */
	public void sendResponse(String response) throws IOException
	{
		if (log.isDebugEnabled())
			log.debug("Server: " + response);

		this.writer.print(response + "\r\n");
		this.writer.flush();
	}

	/**
	 * Returns an identifier of the session which is reasonably unique within
	 * an extended time period.
	 */
	public String getSessionId() {
		return sessionId;
	}
	
	/* (non-Javadoc)
	 * @see org.subethamail.smtp.MessageContext#getRemoteAddress()
	 */
	@Override
	public InetSocketAddress getRemoteAddress()
	{
		return (InetSocketAddress)this.socket.getRemoteSocketAddress();
	}

	/* (non-Javadoc)
	 * @see org.subethamail.smtp.MessageContext#getSMTPServer()
	 */
	@Override
	public SMTPServer getSMTPServer()
	{
		return this.server;
	}

	/**
	 * @return the current message handler
	 */
	public MessageHandler getMessageHandler()
	{
		return this.messageHandler;
	}

	/** Simple state */
	@Override
	public String getHelo()
	{
		return this.helo;
	}

	/** */
	public void setHelo(String value)
	{
		this.helo = value;
	}

	/** @deprecated use {@link #isMailTransactionInProgress()} */
	@Deprecated
	public boolean getHasMailFrom()
	{
		return isMailTransactionInProgress();
	}

	/** */
	public void addRecipient(String recipientAddress)
	{
		this.recipientCount++;
		this.singleRecipient = this.recipientCount == 1 ? recipientAddress : null;
	}

	/** */
	public int getRecipientCount()
	{
		return this.recipientCount;
	}

	/**
	 * Returns the first accepted recipient if there is exactly one accepted
	 * recipient, otherwise it returns null.
	 */
	public String getSingleRecipient()
	{
		return singleRecipient;
	}

	/** */
	public boolean isAuthenticated()
	{
		return this.authenticationHandler != null;
	}

	/** */
	@Override
	public AuthenticationHandler getAuthenticationHandler()
	{
		return this.authenticationHandler;
	}

	/**
	 * This is called by the AuthCommand when a session is successfully authenticated.  The
	 * handler will be an object created by the AuthenticationHandlerFactory.
	 */
	public void setAuthenticationHandler(AuthenticationHandler handler)
	{
		this.authenticationHandler = handler;
	}

	/**
	 * @return the maxMessageSize
	 */
	public int getDeclaredMessageSize()
	{
		return this.declaredMessageSize;
	}

	/**
	 * @param declaredMessageSize the size that the client says the message will be
	 */
	public void setDeclaredMessageSize(int declaredMessageSize)
	{
		this.declaredMessageSize = declaredMessageSize;
	}

	/**
	 * Starts a mail transaction by creating a new message handler.
	 * 
	 * @throws IllegalStateException
	 *             if a mail transaction is already in progress
	 */
	public void startMailTransaction() throws IllegalStateException {
		if (this.messageHandler != null) 
			throw new IllegalStateException(
					"Mail transaction is already in progress");
		this.messageHandler = this.server.getMessageHandlerFactory().create(
				this);
	}

	/**
	 * Returns true if a mail transaction is started, i.e. a MAIL command is
	 * received, and the transaction is not yet completed or aborted. A
	 * transaction is successfully completed after the message content is
	 * received and accepted at the end of the DATA command.
	 */
	public boolean isMailTransactionInProgress() {
		return this.messageHandler != null;
	}
	
	/**
	 * Stops the mail transaction if it in progress and resets all state related
	 * to mail transactions.
	 * <p>
	 * Note: Some state is associated with each particular message (senders,
	 * recipients, the message handler).<br>
	 * Some state is not; seeing hello, TLS, authentication.
	 */
	public void resetMailTransaction()
	{
		this.endMessageHandler();
		this.messageHandler = null;
		this.recipientCount = 0;
		this.singleRecipient = null;
		this.declaredMessageSize = 0;
	}
	
	/** @deprecated use {@link #resetMailTransaction()} */
	@Deprecated
	public void resetMessageState()
	{
		resetMailTransaction();
	}
	
	/** Safely calls done() on a message hander, if one exists */
	private void endMessageHandler()
	{
		if (this.messageHandler != null)
		{
			try
			{
				this.messageHandler.done();
			}
			catch (Throwable ex)
			{
				log.error("done() threw exception", ex);
			}
		}
	}
	
	/**
	 * Reset the SMTP protocol to the initial state, which is the state after 
	 * a server issues a 220 service ready greeting. 
	 */
	public void resetSmtpProtocol() {
		resetMailTransaction();
		this.helo = null;
	}
	
	/**
	 * Triggers the shutdown of the thread and the closing of the connection.
	 */
	public void quit()
	{
		this.quitting = true;
		this.closeConnection();
	}

	/**
	 * @return true when the TLS handshake was completed, false otherwise
	 */
	public boolean isTLSStarted()
	{
		return tlsStarted;
	}

	/**
	 * @param tlsStarted true when the TLS handshake was completed, false otherwise
	 */
	public void setTlsStarted(boolean tlsStarted)
	{
		this.tlsStarted = tlsStarted;
	}

	public void setTlsPeerCertificates(Certificate[] tlsPeerCertificates)
	{
		this.tlsPeerCertificates = tlsPeerCertificates;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Certificate[] getTlsPeerCertificates()
	{
		return tlsPeerCertificates;
	}
}