All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.subethamail.smtp.server.SMTPServer Maven / Gradle / Ivy

package org.subethamail.smtp.server;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;

import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.DefaultIoFilterChainBuilder;
import org.apache.mina.common.SimpleByteBufferAllocator;
import org.apache.mina.common.ThreadModel;
import org.apache.mina.filter.LoggingFilter;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.executor.ExecutorFilter;
import org.apache.mina.integration.jmx.IoServiceManager;
import org.apache.mina.transport.socket.nio.SocketAcceptor;
import org.apache.mina.transport.socket.nio.SocketAcceptorConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.smtp.MessageHandlerFactory;
import org.subethamail.smtp.MessageListener;
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 Mina 
 * based listener 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.
 *  
 * There are two ways of using this server.  The first is to
 * construct with a MessageHandlerFactory.  This provides the
 * lowest-level and most flexible access.  The second way is
 * to construct with a collection of MessageListeners.  This
 * is a higher, and sometimes more convenient level of abstraction.
 * 
 * In neither case is the SMTP server (this library) responsible
 * for deciding what recipients to accept or what to do with the
 * incoming data.  That is left to you.
 * 
 * @author Jon Stevens
 * @author Ian McFarland <[email protected]>
 * @author Jeff Schnitzer
 * 
 * This file has been used and differs from the original
 * by the use of MINA NIO framework.
 * 
 * @author De Oliveira Edouard <[email protected]>
 */
public class SMTPServer
{
	private static Logger log = LoggerFactory.getLogger(SMTPServer.class);

	public final static Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");

	/**
	 * default to all interfaces
	 */
	private InetAddress bindAddress = null;

	/**
	 * default to 25
	 */
	private int port = 25;

	/**
	 * defaults to a lookup of the local address
	 */
	private String hostName;

	/**
	 * defaults to 5000
	 */
	private int backlog = 5000;
	
	private MessageHandlerFactory messageHandlerFactory;
	private CommandHandler commandHandler;
	private SocketAcceptor acceptor;
	private ExecutorService executor;
	private ExecutorService acceptorThreadPool;
	private SocketAcceptorConfig config;
	private IoServiceManager serviceManager;
	private ObjectName jmxName;
	private ConnectionHandler handler;
	private SMTPCodecDecoder codecDecoder;
	private boolean go = 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;
	
	/**
	 * 4 megs by default. The server will buffer incoming messages to disk
	 * when they hit this limit in the DATA received.
	 */
	protected final static int DEFAULT_DATA_DEFERRED_SIZE = 1024*1024*4;
	
	private int dataDeferredSize = DEFAULT_DATA_DEFERRED_SIZE;
	
	private boolean announceTls = true;

	/**
	 * The primary constructor.
	 */
	public SMTPServer(MessageHandlerFactory handlerFactory)
	{
		this.messageHandlerFactory = handlerFactory;

		try
		{
			this.hostName = InetAddress.getLocalHost().getCanonicalHostName();
		}
		catch (UnknownHostException e)
		{
			this.hostName = "localhost";
		}

		this.commandHandler = new CommandHandler();
		initService();
	}

	/**
	 * A convenience constructor that splits the smtp data among multiple listeners
	 * (and multiple recipients).
	 */
	public SMTPServer(Collection listeners)
	{
		this(new MessageListenerAdapter(listeners));
	}

	/**
	 * Starts the JMX service with a polling interval default of 1000ms.
	 * 
	 * @throws InstanceAlreadyExistsException
	 * @throws MBeanRegistrationException
	 * @throws NotCompliantMBeanException
	 */
	public void startJMXService()
		throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException
	{
		startJMXService(1000);
	}

	/**
	 * Start the JMX service.
	 * 
	 * @param pollingInterval
	 * @throws InstanceAlreadyExistsException
	 * @throws MBeanRegistrationException
	 * @throws NotCompliantMBeanException
	 */
	public void startJMXService(int pollingInterval)
		throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException
	{
		serviceManager.startCollectingStats(pollingInterval);
		MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
		mbs.registerMBean(serviceManager, jmxName);
	}

	/**
	 * Stop the JMX service.
	 * 
	 * @throws InstanceNotFoundException
	 * @throws MBeanRegistrationException
	 */
	public void stopJMXService() throws InstanceNotFoundException, MBeanRegistrationException
	{
		serviceManager.stopCollectingStats();
		MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
		mbs.unregisterMBean(jmxName);
	}

	/**
	 * Initializes the runtime service.
	 */
	private void initService()
	{
		try
		{
			ByteBuffer.setUseDirectBuffers(false);
			ByteBuffer.setAllocator(new SimpleByteBufferAllocator());

			acceptorThreadPool = Executors.newCachedThreadPool();
			acceptor =
				new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, acceptorThreadPool);

			// JMX instrumentation
			serviceManager = new IoServiceManager(acceptor);
			jmxName = new ObjectName("subethasmtp.mina.server:type=IoServiceManager");

			config = new SocketAcceptorConfig();
			config.setThreadModel(ThreadModel.MANUAL);
			((SocketAcceptorConfig)config).setReuseAddress(true);

			DefaultIoFilterChainBuilder chain = config.getFilterChain();

			if (log.isTraceEnabled())
				chain.addLast("logger", new LoggingFilter());

			SMTPCodecFactory codecFactory = new SMTPCodecFactory(DEFAULT_CHARSET, getDataDeferredSize());
			codecDecoder = (SMTPCodecDecoder) codecFactory.getDecoder();
			chain.addLast("codec", new ProtocolCodecFilter(codecFactory));

			executor = Executors.newCachedThreadPool(new ThreadFactory() {
				int sequence;
				
				public Thread newThread(Runnable r) 
				{					
					sequence += 1;
					return new Thread(r, "SubEthaSMTP Thread "+sequence);
				}			
			});
			
			chain.addLast("threadPool", new ExecutorFilter(executor));
			
			handler = new ConnectionHandler(this);
		}
		catch (Exception ex)
		{
			throw new RuntimeException(ex);
		}
	}

	/**
	 * Call this method to get things rolling after instantiating the
	 * SMTPServer.
	 */
	public synchronized void start()
	{
		if (go == true)
			throw new RuntimeException("SMTPServer is already started.");
		
		InetSocketAddress isa;

		if (this.bindAddress == null)
		{
			isa = new InetSocketAddress(this.port);
		}
		else
		{
			isa = new InetSocketAddress(this.bindAddress, this.port);
		}

		((SocketAcceptorConfig)config).setBacklog(getBacklog());

		try
		{
			acceptor.bind(isa, handler, config);
			go = true;
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
	}

	/**
	 * Shut things down gracefully.
	 */
	public synchronized void stop()
	{
		try
		{
			log.info("SMTP Server socket shut down.");
			try { acceptor.unbindAll(); } catch (Exception e) { }
			try { executor.shutdown(); } catch (Exception e) { }
			try { acceptorThreadPool.shutdown(); } catch (Exception e) { }
		}
		finally
		{
			go = false;
		}
	}

	/**
	 * Tells the server to announce the TLS support. Defaults to true. 
	 */
	public void setAnnounceTLS(boolean announceTls)
	{
		this.announceTls = announceTls;
	}
	
	/**
	 * @return true if server is allowed to announce TLS support.
	 */
	public boolean announceTLS()
	{
		return announceTls;
	}

	/** 
	 * @return the host name that will be reported to SMTP clients 
	 */
	public String getHostName()
	{
		if (this.hostName == null)
			return "localhost";
		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;
	}

	/**
	 * get the port the server is running on.
	 */
	public int getPort()
	{
		return this.port;
	}

	/**
	 * set the port the server is running on.
	 * @param port
	 */
	public void setPort(int port)
	{
		this.port = port;
	}

	/**
	 * Is the server running after start() has been called?
	 */
	public synchronized boolean isRunning()
	{
		return this.go;
	}

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

	/**
	 * The name of the server software.
	 */
	public String getName()
	{
		return "SubEthaSMTP";
	}

	/**
	 * The name + version of the server software.
	 */
	public String getNameVersion()
	{
		return getName() + " " + Version.getSpecification();
	}

	/**
	 * All smtp data is eventually routed through the handlers.
	 */
	public MessageHandlerFactory getMessageHandlerFactory()
	{
		return this.messageHandlerFactory;
	}

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

	/**
	 * Number of connections in the handler.
	 */
	public int getNumberOfConnections()
	{
		return handler.getNumberOfConnections();
	}

	/**
	 * Are we over the maximum amount of connections ?
	 */
	public boolean hasTooManyConnections()
	{
		return (this.maxConnections > -1 && 
				getNumberOfConnections() >= this.maxConnections);
	}

	/**
	 * What is the maximum amount of connections?
	 */
	public int getMaxConnections()
	{
		return this.maxConnections;
	}

	/**
	 * Set's the maximum number of connections this server instance will
	 * accept. If set to -1 then limit is ignored.
	 * 
	 * @param maxConnections
	 */
	public void setMaxConnections(int maxConnections)
	{
		this.maxConnections = maxConnections;
	}

	/**
	 * What is the connection timeout?
	 */
	public int getConnectionTimeout()
	{
		return this.connectionTimeout;
	}

	/**
	 * Set the connection timeout.
	 */
	public void setConnectionTimeout(int connectionTimeout)
	{
		this.connectionTimeout = connectionTimeout;
	}

	/**
	 * What is the maximum number of recipients for a single message ?
	 */
	public int getMaxRecipients()
	{
		return this.maxRecipients;
	}

	/**
	 * Set the maximum number of recipients for a single message.
	 * If set to -1 then limit is ignored.
	 */
	public void setMaxRecipients(int maxRecipients)
	{
		this.maxRecipients = maxRecipients;
	}

	/**
	 * Get the maximum size in bytes of a single message before it is 
	 * dumped to a temporary file.
	 */	
	public int getDataDeferredSize() 
	{
		return dataDeferredSize;
	}

	/**
	 * Set the maximum size in bytes of a single message before it is 
	 * dumped to a temporary file. Argument must be a positive power 
	 * of two in order to follow the expanding algorithm of 
	 * {@link org.apache.mina.common.ByteBuffer} to prevent unnecessary
	 * memory consumption.
	 */	
	public void setDataDeferredSize(int dataDeferredSize) 
	{
		if (isPowerOfTwo(dataDeferredSize))
		{
			this.dataDeferredSize = dataDeferredSize;
			if (codecDecoder != null)
				codecDecoder.setDataDeferredSize(dataDeferredSize);
		}
		else
			throw new IllegalArgumentException(
					"Argument dataDeferredSize must be a positive power of two");
	}
	
	/**
	 * Sets the receive buffer size.
	 */
	public void setReceiveBufferSize(int receiveBufferSize)
	{
		handler.setReceiveBufferSize(receiveBufferSize);
	}	
	
	/**
	 * Demonstration : if x is a power of 2, it can't share any bit with x-1. So 
	 * x & (x-1) should be equal to 0. To get rid of negative values, we check
	 * that x is higher than 1 (0 and 1 being of course unacceptable values 
	 * for a buffer length). 
	 * 
	 * @param x the number to test
	 * @return true if x is a power of two
	 */
	protected boolean isPowerOfTwo(int x)
	{
		return (x > 1) && (x & (x-1)) == 0;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy