package org.subethamail.smtp.server;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.annotation.concurrent.GuardedBy;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.smtp.AuthenticationHandlerFactory;
import org.subethamail.smtp.MessageHandlerFactory;
import org.subethamail.smtp.Version;
/**
* Main SMTPServer class. Construct this object, set the
* hostName, port, and bind address if you wish to override the
* defaults, and call start().
*
* This class starts opens a ServerSocket and creates a new
* instance of the ConnectionHandler class when a new connection
* comes in. The ConnectionHandler then parses the incoming SMTP
* stream and hands off the processing to the CommandHandler which
* will execute the appropriate SMTP command class.
*
* This class also manages a watchdog thread which will timeout
* stale connections.
*
* To use this class, construct a server with your implementation
* of the MessageHandlerFactory. This provides low-level callbacks
* at various phases of the SMTP exchange. For a higher-level
* but more limited interface, you can pass in a
* org.subethamail.smtp.helper.SimpleMessageListenerAdapter.
*
* By default, no authentication methods are offered. To use
* authentication, set an AuthenticationHandlerFactory.
*
* @author Jon Stevens
* @author Ian McFarland <ian@neo.com>
* @author Jeff Schnitzer
*/
public class SMTPServer
{
private final static Logger log = LoggerFactory.getLogger(SMTPServer.class);
/** Hostname used if we can't find one */
private final static String UNKNOWN_HOSTNAME = "localhost";
private InetAddress bindAddress = null; // default to all interfaces
private int port = 25; // default to 25
private String hostName; // defaults to a lookup of the local address
private int backlog = 50;
private String softwareName = "SubEthaSMTP " + Version.getSpecification();
private MessageHandlerFactory messageHandlerFactory;
private AuthenticationHandlerFactory authenticationHandlerFactory;
private CommandHandler commandHandler;
/** The thread listening on the server socket. */
@GuardedBy("this")
private ServerThread serverThread;
/** If true, TLS is enabled */
private boolean enableTLS = false;
/** If true, TLS is not announced; ignored if enableTLS=false */
private boolean hideTLS = false;
/** If true, a TLS handshake is required; ignored if enableTLS=false */
private boolean requireTLS = false;
/** If true, no Received headers will be inserted */
private boolean disableReceivedHeaders = false;
/**
* set a hard limit on the maximum number of connections this server will accept
* once we reach this limit, the server will gracefully reject new connections.
* Default is 1000.
*/
private int maxConnections = 1000;
/**
* The timeout for waiting for data on a connection is one minute: 1000 * 60 * 1
*/
private int connectionTimeout = 1000 * 60 * 1;
/**
* The maximal number of recipients that this server accepts per message delivery request.
*/
private int maxRecipients = 1000;
/**
* The maximum size of a message that the server will accept. This value is advertised
* during the EHLO phase if it is larger than 0. If the message size specified by the client
* during the MAIL phase, the message will be rejected at that time. (RFC 1870)
* Default is 0. Note this doesn't actually enforce any limits on the message being
* read; you must do that yourself when reading data.
*/
private int maxMessageSize = 0;
/**
* The primary constructor.
*/
public SMTPServer(MessageHandlerFactory handlerFactory)
{
this(handlerFactory, null);
}
/**
* The primary constructor.
*/
public SMTPServer(MessageHandlerFactory msgHandlerFact, AuthenticationHandlerFactory authHandlerFact)
{
this.messageHandlerFactory = msgHandlerFact;
this.authenticationHandlerFactory = authHandlerFact;
try
{
this.hostName = InetAddress.getLocalHost().getCanonicalHostName();
}
catch (UnknownHostException e)
{
this.hostName = UNKNOWN_HOSTNAME;
}
this.commandHandler = new CommandHandler();
}
/** @return the host name that will be reported to SMTP clients */
public String getHostName()
{
if (this.hostName == null)
return UNKNOWN_HOSTNAME;
else
return this.hostName;
}
/** The host name that will be reported to SMTP clients */
public void setHostName(String hostName)
{
this.hostName = hostName;
}
/** null means all interfaces */
public InetAddress getBindAddress()
{
return this.bindAddress;
}
/** null means all interfaces */
public void setBindAddress(InetAddress bindAddress)
{
this.bindAddress = bindAddress;
}
/** */
public int getPort()
{
return this.port;
}
/** */
public void setPort(int port)
{
this.port = port;
}
/**
* The string reported to the public as the software running here. Defaults
* to SubEthaSTP and the version number.
*/
public String getSoftwareName()
{
return this.softwareName;
}
/**
* Changes the publicly reported software information.
*/
public void setSoftwareName(String value)
{
this.softwareName = value;
}
/**
* Is the server running after start() has been called?
*/
public synchronized boolean isRunning()
{
return this.serverThread != null;
}
/**
* The backlog is the Socket backlog.
*
* The backlog argument must be a positive value greater than 0.
* If the value passed if equal or less than 0, then the default value will be assumed.
*
* @return the backlog
*/
public int getBacklog()
{
return this.backlog;
}
/**
* The backlog is the Socket backlog.
*
* The backlog argument must be a positive value greater than 0.
* If the value passed if equal or less than 0, then the default value will be assumed.
*/
public void setBacklog(int backlog)
{
this.backlog = backlog;
}
/**
* Call this method to get things rolling after instantiating the
* SMTPServer.
*/
public synchronized void start()
{
if (log.isInfoEnabled())
log.info("SMTP server {} starting", getDisplayableLocalSocketAddress());
if (this.serverThread != null)
throw new IllegalStateException("SMTPServer already started");
// Create our server socket here.
ServerSocket serverSocket;
try
{
serverSocket = this.createServerSocket();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
this.serverThread = new ServerThread(this, serverSocket);
this.serverThread.start();
}
/**
* Shut things down gracefully.
*/
public synchronized void stop()
{
if (log.isInfoEnabled())
log.info("SMTP server {} stopping", getDisplayableLocalSocketAddress());
if (this.serverThread == null)
return;
this.serverThread.shutdown();
this.serverThread = null;
}
/**
* Override this method if you want to create your own server sockets.
* You must return a bound ServerSocket instance
*
* @throws IOException
*/
protected ServerSocket createServerSocket() throws IOException
{
InetSocketAddress isa;
if (this.bindAddress == null)
{
isa = new InetSocketAddress(this.port);
}
else
{
isa = new InetSocketAddress(this.bindAddress, this.port);
}
ServerSocket serverSocket = new ServerSocket();
// http://java.sun.com/j2se/1.5.0/docs/api/java/net/ServerSocket.html#setReuseAddress(boolean)
serverSocket.setReuseAddress(true);
serverSocket.bind(isa, this.backlog);
if (this.port == 0)
{
this.port = serverSocket.getLocalPort();
}
return serverSocket;
}
/**
* Create a SSL socket that wraps the existing socket. This method
* is called after the client issued the STARTTLS command.
* <p>
* Subclasses may override this method to configure the key stores, enabled protocols/
* cipher suites, enforce client authentication, etc.
*
* @param socket the existing socket as created by {@link #createServerSocket()} (not null)
* @return a SSLSocket
* @throws IOException when creating the socket failed
*/
public SSLSocket createSSLSocket(Socket socket) throws IOException
{
SSLSocketFactory sf = ((SSLSocketFactory) SSLSocketFactory.getDefault());
InetSocketAddress remoteAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
SSLSocket s = (SSLSocket) (sf.createSocket(socket, remoteAddress.getHostName(), socket.getPort(), true));
// we are a server
s.setUseClientMode(false);
// allow all supported cipher suites
s.setEnabledCipherSuites(s.getSupportedCipherSuites());
return s;
}
public String getDisplayableLocalSocketAddress()
{
return (this.bindAddress == null ? "*" : this.bindAddress) + ":" + this.port;
}
/**
* @return the factory for message handlers, cannot be null
*/
public MessageHandlerFactory getMessageHandlerFactory()
{
return this.messageHandlerFactory;
}
/** */
public void setMessageHandlerFactory(MessageHandlerFactory fact)
{
this.messageHandlerFactory = fact;
}
/**
* @return the factory for auth handlers, or null if no such factory has been set.
*/
public AuthenticationHandlerFactory getAuthenticationHandlerFactory()
{
return this.authenticationHandlerFactory;
}
/** */
public void setAuthenticationHandlerFactory(AuthenticationHandlerFactory fact)
{
this.authenticationHandlerFactory = fact;
}
/**
* The CommandHandler manages handling the SMTP commands
* such as QUIT, MAIL, RCPT, DATA, etc.
*
* @return An instance of CommandHandler
*/
public CommandHandler getCommandHandler()
{
return this.commandHandler;
}
/** */
public int getMaxConnections()
{
return this.maxConnections;
}
/**
* Set's the maximum number of connections this server instance will accept.
*
* @param maxConnections
*/
public void setMaxConnections(int maxConnections)
{
if (this.isRunning())
throw new RuntimeException("Server is already running. It isn't possible to set the maxConnections. Please stop the server first.");
this.maxConnections = maxConnections;
}
/** */
public int getConnectionTimeout()
{
return this.connectionTimeout;
}
/**
* Set the number of milliseconds that the server will wait for
* client input. Sometime after this period expires, an client will
* be rejected and the connection closed.
*/
public void setConnectionTimeout(int connectionTimeout)
{
this.connectionTimeout = connectionTimeout;
}
public int getMaxRecipients()
{
return this.maxRecipients;
}
/**
* Set the maximum number of recipients allowed for each message.
* A value of -1 means "unlimited".
*/
public void setMaxRecipients(int maxRecipients)
{
this.maxRecipients = maxRecipients;
}
/**
* If set to true, TLS will be supported.
* <p>
* The minimal JSSE configuration necessary for a working TLS support on
* Oracle JRE 6:
* <ul>
* <li>javax.net.ssl.keyStore system property must refer to a file
* containing a JKS keystore with the private key.
* <li>javax.net.ssl.keyStorePassword system property must specify the
* keystore password.
* </ul>
* <p>
* Up to SubEthaSMTP 3.1.5 the default was true, i.e. TLS was enabled.
*
* @see <a
* href="http://blog.jteam.nl/2009/11/10/securing-connections-with-tls/">Securing
* Connections with TLS</a>
*/
public void setEnableTLS(boolean enableTLS)
{
this.enableTLS = enableTLS;
}
/** */
public boolean getEnableTLS()
{
return enableTLS;
}
/**
* @deprecated use {@link #enableTLS}
*/
@Deprecated
public boolean getDisableTLS()
{
return !this.enableTLS;
}
/**
* @deprecated use {@link #setEnableTLS(boolean)}
*/
@Deprecated
public void setDisableTLS(boolean value)
{
this.enableTLS = !value;
}
/** */
public boolean getHideTLS()
{
return this.hideTLS;
}
/**
* If set to true, TLS will not be advertised in the EHLO string.
* Default is false; true implied when disableTLS=true.
*/
public void setHideTLS(boolean value)
{
this.hideTLS = value;
}
/** */
public boolean getRequireTLS()
{
return this.requireTLS;
}
/**
* @param requireTLS true to require a TLS handshake,
* false to allow operation with or without TLS.
* Default is false; ignored when disableTLS=true.
*/
public void setRequireTLS(boolean requireTLS)
{
this.requireTLS = requireTLS;
}
/**
* @return the maxMessageSize
*/
public int getMaxMessageSize()
{
return maxMessageSize;
}
/**
* @param maxMessageSize
* the maxMessageSize to set
*/
public void setMaxMessageSize(int maxMessageSize)
{
this.maxMessageSize = maxMessageSize;
}
/** */
public boolean getDisableReceivedHeaders()
{
return disableReceivedHeaders;
}
/**
* @param disableReceivedHeaders
* false to include Received headers. Default is false.
*/
public void setDisableReceivedHeaders(boolean disableReceivedHeaders)
{
this.disableReceivedHeaders = disableReceivedHeaders;
}
}