/*
 * fakesmtp - A fake SMTP server used by the nmh test suite
 *
 * This code is Copyright (c) 2012, by the authors of nmh.  See the
 * COPYRIGHT file in the root directory of the nmh distribution for
 * complete copyright information.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <signal.h>

#define PIDFILE "/tmp/fakesmtp.pid"

#define LINESIZE 1024

static void killpidfile(void);
static void handleterm(int);
static void putsmtp(int, char *);
static int getsmtp(int, char *);

int
main(int argc, char *argv[])
{
	struct addrinfo hints, *res;
	int rc, l, conn, on, datamode;
	FILE *f, *pid;
	fd_set readfd;
	struct stat st;
	struct timeval tv;

	if (argc != 3) {
		fprintf(stderr, "Usage: %s output-filename port\n", argv[0]);
		exit(1);
	}

	if (!(f = fopen(argv[1], "w"))) {
		fprintf(stderr, "Unable to open output file \"%s\": %s\n",
			argv[1], strerror(errno));
		exit(1);
	}

	/*
	 * If there is a pid file already around, kill the previously running
	 * fakesmtp process.  Hopefully this will reduce the race conditions
	 * that crop up when running the test suite.
	 */

	if (stat(PIDFILE, &st) == 0) {
		long oldpid;

		if (!(pid = fopen(PIDFILE, "r"))) {
			fprintf(stderr, "Cannot open " PIDFILE
				" (%s), continuing ...\n", strerror(errno));
		} else {
			rc = fscanf(pid, "%ld", &oldpid);
			fclose(pid);

			if (rc != 1) {
				fprintf(stderr, "Unable to parse pid in "
					PIDFILE ", continuing ...\n");
			} else {
				kill((pid_t) oldpid, SIGTERM);
			}
		}

		unlink(PIDFILE);
	}

	memset(&hints, 0, sizeof(hints));

	hints.ai_family = PF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	hints.ai_flags = AI_PASSIVE;

	rc = getaddrinfo("127.0.0.1", argv[2], &hints, &res);

	if (rc) {
		fprintf(stderr, "Unable to resolve localhost/%s: %s\n",
			argv[2], gai_strerror(rc));
		exit(1);
	}

	l = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

	if (l == -1) {
		fprintf(stderr, "Unable to create listening socket: %s\n",
			strerror(errno));
		exit(1);
	}

	on = 1;

	if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
		fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n",
			strerror(errno));
		exit(1);
	}

	if (bind(l, res->ai_addr, res->ai_addrlen) == -1) {
		fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno));
		exit(1);
	}

	if (listen(l, 1) == -1) {
		fprintf(stderr, "Unable to listen on socket: %s\n",
			strerror(errno));
		exit(1);
	}

	/*
	 * Now that our socket & files are set up, wait 30 seconds for
	 * a connection.  If there isn't one, then exit.
	 */

	if (!(pid = fopen(PIDFILE, "w"))) {
		fprintf(stderr, "Cannot open " PIDFILE ": %s\n",
			strerror(errno));
		exit(1);
	}

	fprintf(pid, "%ld\n", (long) getpid());
	fclose(pid);

	signal(SIGTERM, handleterm);
	atexit(killpidfile);

	FD_ZERO(&readfd);
	FD_SET(l, &readfd);
	tv.tv_sec = 30;
	tv.tv_usec = 0;

	rc = select(l + 1, &readfd, NULL, NULL, &tv);

	if (rc < 0) {
		fprintf(stderr, "select() failed: %s\n", strerror(errno));
		exit(1);
	}

	/*
	 * I think if we get a timeout, we should just exit quietly.
	 */

	if (rc == 0) {
		exit(1);
	}

	/*
	 * Alright, got a connection!  Accept it.
	 */

	if ((conn = accept(l, NULL, NULL)) == -1) {
		fprintf(stderr, "Unable to accept connection: %s\n",
			strerror(errno));
		exit(1);
	}

	close(l);
	
	/*
	 * Pretend to be an SMTP server.
	 */

	putsmtp(conn, "220 Not really an ESMTP server");
	datamode = 0;

	for (;;) {
		char line[LINESIZE];

		rc = getsmtp(conn, line);

		if (rc == -1)
			break;	/* EOF */

		fprintf(f, "%s\n", line);

		/*
		 * If we're in DATA mode, then check to see if we've got
		 * a "."; otherwise, continue
		 */

		if (datamode) {
			if (strcmp(line, ".") == 0) {
				datamode = 0;
				putsmtp(conn, "250 Thanks for the info!");
			}
			continue;
		}

		/*
		 * Most commands we ignore and send the same response to.
		 */

		if (strcmp(line, "QUIT") == 0) {
			putsmtp(conn, "221 Later alligator!");
			close(conn);
			break;
		} else if (strcmp(line, "DATA") == 0) {
			putsmtp(conn, "354 Go ahead");
			datamode = 1;
		} else {
			putsmtp(conn, "250 I'll buy that for a dollar!");
		}
	}

	fclose(f);

	exit(0);
}

/*
 * Write a line to the SMTP client on the other end
 */

static void
putsmtp(int socket, char *data)
{
	struct iovec iov[2];

	iov[0].iov_base = data;
	iov[0].iov_len = strlen(data);
	iov[1].iov_base = "\r\n";
	iov[1].iov_len = 2;

	writev(socket, iov, 2);
}

/*
 * Read a line (up to the \r\n)
 */

static int
getsmtp(int socket, char *data)
{
	int cc;
	static unsigned int bytesinbuf = 0;
	static char buffer[LINESIZE * 2], *p;

	for (;;) {
		/*
		 * Find our \r\n
		 */

		if (bytesinbuf > 0 && (p = strchr(buffer, '\r')) &&
							*(p + 1) == '\n') {
			*p = '\0';
			strncpy(data, buffer, LINESIZE);
			data[LINESIZE - 1] = '\0';
			cc = strlen(buffer);

			/*
			 * Shuffle leftover bytes back to the beginning
			 */

			bytesinbuf -= cc + 2;	/* Don't forget \r\n */
			if (bytesinbuf > 0) {
				memmove(buffer, buffer + cc + 2, bytesinbuf);
			}
			return cc;
		}

		if (bytesinbuf >= sizeof(buffer)) {
			fprintf(stderr, "Buffer overflow in getsmtp()!\n");
			exit(1);
		}

		memset(buffer + bytesinbuf, 0, sizeof(buffer) - bytesinbuf);
		cc = read(socket, buffer + bytesinbuf,
			  sizeof(buffer) - bytesinbuf);

		if (cc < 0) {
			fprintf(stderr, "Read failed: %s\n", strerror(errno));
			exit(1);
		}

		if (cc == 0)
			return -1;

		bytesinbuf += cc;
	}
}

/*
 * Handle a SIGTERM
 */

static void
handleterm(int signal)
{
	(void) signal;

	killpidfile();
	fflush(NULL);
	_exit(1);
}

/*
 * Get rid of our pid file
 */

static void
killpidfile(void)
{
	unlink(PIDFILE);
}
