/*
 * Copyright (C) 2004 Liam Widdowson (liam@inodes.org)
 *  
 * smtptrapd - The SMTP Trap Daemon - Version 1.3
 * 
 * Last updated: Tue Jul  4 00:02:47 EDT 2006
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include <stdio.h>
#include <pthread.h>
#include <pwd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <sysexits.h>
#include <syslog.h>
#include <signal.h>
#ifdef __linux__
#include <getopt.h>
#endif

#define LISTEN_PORT 25
#define NUM_CONSUMER 10
#define UNPRIV_USER "nobody"

#define LOG_SIZ 80

#define SMTP_STATE_NONE 0x0
#define SMTP_STATE_HELO 0x1
#define SMTP_STATE_FROM 0x2
#define SMTP_STATE_RCPT 0x4
#define SMTP_STATE_QUIT 0x8

#define SMTP_CMD_MAX 50
#define SMTP_CMD_TOUT 60
#define SMTP_TIME_MAX 120
#define MAX_QUEUE_LEN 100

#define SMTP_MSG_BANNERBYE "421 %s Service Unavailable\r\n"
#define SMTP_MSG_BANNER "220 %s ESMTP Service Ready\r\n"
#define SMTP_MSG_GOODBYE "221 Closing connection\r\n"
#define SMTP_MSG_BADBYE "421 Closing connection\r\n"
#define SMTP_MSG_OK "250 OK\r\n"
#define SMTP_MSG_TRYAGAIN "451 Try again later\r\n"
#define SMTP_MSG_INVALID "501 Invalid syntax\r\n"

typedef struct {
	int **fd;              /* array of queue elements to be worked upon */
	int fd_num;            /* number of elements in the queue array */
	int work;              /* predicate to indicate is work to be done */
	pthread_cond_t  cond;  /* the condition for consumers to wait upon */
	pthread_mutex_t mutex; /* the associated mutex */
} cqueue_t;

typedef struct {
	char *chroot_dir;
	char *banner;
	char *username;
	struct in_addr local_ip;
	short bind_local;
	int port;
	int max_queue_len;
	int threads;
} config_t;

cqueue_t queue;
config_t config;

void chomp(char *buf, size_t len) {
	size_t x;
	for (x = 0; x < len; x++) {
		if ((buf[x] == '\r') || (buf[x] == '\n')) buf[x] = '\0';
	}
}

/* a function to bind to a particular TCP port */
int bind_proc(void) {
	struct sockaddr_in saddr;
	int s;
	int one = 1;

	saddr.sin_family = AF_INET;
	if (config.bind_local) {
		saddr.sin_addr.s_addr = config.local_ip.s_addr;
	} else {
		saddr.sin_addr.s_addr = htonl(INADDR_ANY);
	}
	saddr.sin_port = htons(config.port);

	if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		return -1;
	if (setsockopt(s, SOL_SOCKET,SO_REUSEADDR, &one, sizeof(one)) == -1) 
		return -1;
	if (bind(s, (struct sockaddr *) &saddr, sizeof(saddr)) == -1) 
		return -1;
	if (listen(s, 20) == -1) 
		return -1;

	return s;
}

int drop_priv(void) {
	struct passwd *p;

	p = getpwnam((config.username == NULL) ?  
	               UNPRIV_USER : config.username);

	if (p == NULL) {
		syslog(LOG_CRIT, "error: could not find uid for %s",
		(config.username == NULL) ?  UNPRIV_USER : config.username);
		return -1;
	}

	if (config.chroot_dir != NULL) {

		if (chdir(config.chroot_dir) != 0)
		{
			syslog(LOG_CRIT, "error: could not chdir to %s", 
			config.chroot_dir);
			return -1;
		}

		if (chroot(config.chroot_dir) != 0)
		{
			syslog(LOG_CRIT, "error: could not chroot to %s", 
			config.chroot_dir);
			return -1;
		}
	}

	if (setgid(p->pw_gid) != 0) {
		syslog(LOG_CRIT, "error: could not find setgid for %s",
		(config.username == NULL) ?  UNPRIV_USER : config.username);
		return -1;
	}
	if (setuid(p->pw_uid) != 0) {
		syslog(LOG_CRIT, "error: could not find setgid for %s",
		(config.username == NULL) ?  UNPRIV_USER : config.username);
		return -1;
	}
	return 0;
}


void producer(int s) {
	struct sockaddr_in caddr;
	size_t clen = sizeof(struct sockaddr_in);
	int ns = 0;

	do {

	while ((ns = accept(s, (struct sockaddr *) &caddr, &clen)) != -1) {

		/* lock the queue to add an item */
		pthread_mutex_lock(&queue.mutex);

		/* check to see if the queue is large and if so, simply
		 * politely drop the connection to avoid DoS attacks */

		if (queue.fd_num > config.max_queue_len) {

			char buf[BUFSIZ];
			struct sockaddr_in addr;
			int addrlen = sizeof(addr);

			pthread_mutex_unlock(&queue.mutex);

			snprintf(buf, BUFSIZ-1, SMTP_MSG_BANNERBYE, 
			         config.banner);

			buf[BUFSIZ-1] = '\0';

			if (getpeername(ns, (struct sockaddr *) &addr, 
			                &addrlen) < 0) 
			{
				syslog(LOG_INFO, 
					"dropping inbound connection due to"
				         " excessive queue size");
			} else {
				syslog(LOG_INFO, 
					"dropping inbound connection from: %s"
				         " due to excessive queue size",
					 inet_ntoa(addr.sin_addr));
			}
			write(ns, buf, strlen(buf));
			close(ns);
			continue;
		}

		/* increase the number of items */
		queue.fd_num++;
		
		/* indiciate that there is work to be done */
		queue.work++;

		/* add an item to the queue */
		queue.fd = realloc(queue.fd, (queue.fd_num * sizeof(int *))+1);
		if (queue.fd == NULL) {
			syslog(LOG_CRIT, "error: failed to allocate memory");
			exit(EX_OSERR);
		}
		queue.fd[queue.fd_num] = malloc(sizeof(int));
		if (queue.fd[queue.fd_num] == NULL) {
			syslog(LOG_CRIT, "error: failed to allocate memory");
			exit(EX_OSERR);
		}
		/* set the value of the queue element to the fd */
		*queue.fd[queue.fd_num] = ns;

		/* unlock the queue as the addition has finished */
		pthread_mutex_unlock(&queue.mutex);

		/* wake a thread */
		pthread_cond_signal(&queue.cond);
	}

	} while (errno == ECONNABORTED);

	syslog(LOG_CRIT, "error: accept failed: %s; shutting down", 
	       strerror(errno));
	return;	
}

int read_t(int fd, char *buf, size_t len) {
	int maxfdp1 = fd + 1;
	struct timeval tm = { SMTP_CMD_TOUT, 0 };
	fd_set rset;

	FD_ZERO(&rset);
	FD_SET(fd, &rset);

	if (select(maxfdp1, &rset, NULL, NULL, &tm) != -1) {
		if (FD_ISSET(fd, &rset)) {
			return recv(fd, buf, len, 0);
		} else {
			return -2;
		}
	}
	return 0;
}

void smtp_close(int fd, char *type) {
	write(fd, type, strlen(type));
	close(fd);
}

void smtp_process(int fd) {

	int n;
	int state = SMTP_STATE_NONE;
	char buf[BUFSIZ], from[LOG_SIZ], rcpt[LOG_SIZ];
	struct sockaddr_in addr;
	int addrlen = sizeof(addr);
	int i = 0, etime = 0, itime = time(NULL);

	/* some nice initialisation of strings */
	strcpy(from, "none");
	strcpy(rcpt, "none");

	if (getpeername(fd, (struct sockaddr *) &addr, &addrlen) < 0) {
		smtp_close(fd, SMTP_MSG_BADBYE);
		return;
	}

	/* send the banner */
	snprintf(buf, BUFSIZ-1, SMTP_MSG_BANNER, config.banner);
	buf[BUFSIZ-1] = '\0';

	if (write(fd, buf, strlen(buf)) <= 0) {
		smtp_close(fd, SMTP_MSG_BADBYE);
		return;
	}

	while (state != SMTP_STATE_QUIT) {	

		/* do not let the spammers tie up a thread forever */

		if (i > SMTP_CMD_MAX) {
			smtp_close(fd, SMTP_MSG_BADBYE);
			break;
		}

		etime = time(NULL);

		if (etime > (itime + SMTP_TIME_MAX)) {
			smtp_close(fd, SMTP_MSG_BADBYE);
			break;
		}

		/* wait for a smtp command */
		n = read_t(fd, buf, BUFSIZ-1);

		if (n <= 0) {
			smtp_close(fd, SMTP_MSG_BADBYE);
			break;
		}

		if (n < 4) {
			/* string can not possibly be a real SMTP verb */
			if (write(fd, SMTP_MSG_INVALID, strlen(SMTP_MSG_INVALID)) <= 0) {
				smtp_close(fd, SMTP_MSG_BADBYE);
				break;
			}
			continue;
		}

		/* strip out the CRLF characters and NULL terminate
		 * the string */

		buf[n] = '\0';

		chomp(buf, n);

		i++;

		if ((strncasecmp(buf, "HELO", 4) == 0 || 
		    strncasecmp(buf, "EHLO", 4) == 0) && 
		    state == SMTP_STATE_NONE) 
		{
			state = SMTP_STATE_HELO;
			if (write(fd, SMTP_MSG_OK, strlen(SMTP_MSG_OK)) <= 0) {
				smtp_close(fd, SMTP_MSG_BADBYE);
				break;
			}
		} 

		else  if (strncasecmp(buf, "MAIL FROM", 9) == 0 &&
			  state == SMTP_STATE_HELO)
		{
			state = SMTP_STATE_FROM;
			if (write(fd, SMTP_MSG_OK, strlen(SMTP_MSG_OK)) <= 0) {
				smtp_close(fd, SMTP_MSG_BADBYE);
				break;
			}
			/* for interesting logging */
			strncpy(from, buf, LOG_SIZ-1);
			from[LOG_SIZ-1] = '\0';
		} 
		
		else if (strncasecmp(buf, "RCPT TO", 7) == 0 &&
			 (state == SMTP_STATE_FROM || state == SMTP_STATE_RCPT))
		{
			state = SMTP_STATE_RCPT;
			if (write(fd, SMTP_MSG_TRYAGAIN, strlen(SMTP_MSG_TRYAGAIN)) <= 0) {
				smtp_close(fd, SMTP_MSG_BADBYE);
				break;
			}
			/* for interesting logging; only the last one will
			 * be recorded */
			strncpy(rcpt, buf, LOG_SIZ-1);
			rcpt[LOG_SIZ-1] = '\0';
		} 
		
		else if (strncasecmp(buf, "QUIT", 4) == 0) 
		{
			smtp_close(fd, SMTP_MSG_GOODBYE);
			break;
		}

		else if (strncasecmp(buf, "DATA", 4) == 0 && 
			 state == SMTP_STATE_RCPT) 
		{
			if (write(fd, SMTP_MSG_TRYAGAIN, strlen(SMTP_MSG_TRYAGAIN)) <= 0) {
				smtp_close(fd, SMTP_MSG_BADBYE);
				break;
			}
		}
		else if (strncasecmp(buf, "NOOP", 4) == 0)  {
			if (write(fd, SMTP_MSG_OK, strlen(SMTP_MSG_OK)) <= 0) {
				smtp_close(fd, SMTP_MSG_BADBYE);
				break;
			}
		}
		else {
			if (write(fd, SMTP_MSG_INVALID, strlen(SMTP_MSG_INVALID)) <= 0) {
				smtp_close(fd, SMTP_MSG_BADBYE);
				break;
			}
		}

	}

	syslog(LOG_INFO, "info: connection [ip: %s; f: %s; r: %s]", 
	inet_ntoa(addr.sin_addr), from, rcpt);

	return;
}

void *consumer(void) {
	int fd;

	while (1) {
		/* mutex must be locked before waiting on the condition  */
		pthread_mutex_lock(&queue.mutex);

		/* test predicate to ensure that there is work to be done */
		while (queue.work < 1) {
			/* wait to be called into duty */
			pthread_cond_wait(&queue.cond, &queue.mutex);
		}
		/* returns with mutex locked */

		/* grab the value of the file descriptor from the queue */
		fd = *queue.fd[queue.fd_num];

		/* remove the queue element  */
		free(queue.fd[queue.fd_num]);
		queue.fd_num--;	

		/* indicate that there is now less work to be done */
		queue.work--;

		/* unlock the queue mutex */
		pthread_mutex_unlock(&queue.mutex);	

		smtp_process(fd);
	}
}

int main(int argc, char **argv) {
	pthread_t t;
	int s, x, c;

	/* do some config init */
	config.bind_local = 0;
	config.chroot_dir = NULL;
	config.username = NULL;
	config.banner = NULL;
	config.port = LISTEN_PORT;
	config.threads = NUM_CONSUMER;
	config.max_queue_len = MAX_QUEUE_LEN;

	while ((c = getopt(argc, argv, "hc:l:p:b:u:m:t:")) != -1) {
		switch (c) {
			case 'h':

				printf("%s: %s\n", argv[0],
				       "-c [chroot dir] "
				       "-l [tcp listen address] "
				       "-b [smtp banner hostname] "
				       "-u [username] "
				       "-t [number of threads] "
				       "-p [listen port] "
				       "-m [max accept queue length]");

				exit(EX_USAGE);

			case 'l':
				if (inet_aton(optarg, &config.local_ip) == 0) 
				{
	    				syslog(LOG_CRIT, 
					"error: invalid bind address %s", 
					optarg);
	    				exit(EX_USAGE);
				}
				config.bind_local = 1;
			break;

			case 'c':
				config.chroot_dir = strdup(optarg);
			break;

			case 'b':
				config.banner = strdup(optarg);
			break;

			case 'u':
				config.username = strdup(optarg);
			break;

			case 'p':
				config.port = atoi(optarg);
			break;

			case 't':
				config.threads = atoi(optarg);
			break;

			case 'm':
				config.max_queue_len = atoi(optarg);
			break;
		}
	}

	if (config.banner == NULL) {

		char hname[BUFSIZ];

		memset(hname, 0, BUFSIZ);

		if (gethostname(hname, BUFSIZ-1) != 0) {
			syslog(LOG_CRIT, 
			"error: could not work out my own hostname");
			exit(EX_OSERR);
		}
		config.banner = strdup(hname);
		if (config.banner == NULL) {
			syslog(LOG_CRIT, "error: unable to duplicate string");
			exit(EX_OSERR);
		}
	}

	/* try to bind to the port here as we will most likely need 
	 * to be root */

	if ((s = bind_proc()) == -1) {
		syslog(LOG_CRIT, "error: failed to bind to port %d: %s", 
				config.port, strerror(errno));
		exit(EX_OSERR);
	}

	/* Patch from Shane DeRidder: ignore SIGPIPE from abnormally 
	 * closed connections
	 */
        signal(SIGHUP,  SIG_IGN);
        signal(SIGPIPE, SIG_IGN);

	/* background the process */
	if (fork()) exit(0);
	if (fork()) exit(0);
#ifdef __FreeBSD__
	setpgrp(0, 0);
#else
	setpgrp(); 
#endif

	openlog("smtptrapd", LOG_PID, LOG_MAIL);

	/* drop privileges */

	if (drop_priv() == -1) {
		close(s);
		exit(EX_OSERR);
	}

	/* thread pool initialisation */

	if (pthread_mutex_init(&queue.mutex, NULL) != 0) {
		syslog(LOG_CRIT, "error: failed to initialise mutex");
		close(s);
		exit(EX_OSERR);
	}
	if (pthread_cond_init(&queue.cond, NULL) != 0) {
		syslog(LOG_CRIT, "error: failed to initialise condition");
		close(s);
		exit(EX_OSERR);
	}

	queue.fd = malloc(sizeof(int *));
	if (queue.fd == NULL) {
		syslog(LOG_CRIT, "error: failed to allocate memory");
		close(s);
		exit(EX_OSERR);
	}

	/* initialise queue values */
	queue.fd_num = -1;
	queue.work = 0;

#ifdef __sun__
	pthread_setconcurrency(config.threads);
#endif

	/* create a number of consumer threads */
	for (x = 0; x < config.threads; x++) {
		if (pthread_create(&t, NULL, (void *) consumer, NULL) != 0) {
			syslog(LOG_CRIT, "error: failed to create thread");
			exit(EX_OSERR);
		}
	}

	/* give the thread pool something to do */
	producer(s);

	return 0;
}



syntax highlighted by Code2HTML, v. 0.9.1