/*
 * Simple POP3 client that RETRs and DELEs all mail.
 * Mail is sent to standard output, using ".\n" as an end of mail
 * indicator, and byte-stuffing body lines starting with ".".
 * It is expected that a program will read the output and split it up
 * accordingly and probably send it to sendmail with appropriate bits.
 *
 * This program has been carefully written to avoid buffer overflows.
 * The password is removed from memory after it is used, but because
 * it is sent in the plain on the network, its probably quite pointless
 * to try too much protection. This is why the '-p passwd' option exists.
 *
 * options:
 *    -u user	account name
 *    -p passwd account password
 *    -f file   file containing the account password
 *    -v	verbose mode
 *    -n	don't delete mail after retrieving it
 *
 * David Leonard, 2004. Public domain.
 */

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <err.h>

#define	CR	'\r'
#define	LF	'\n'

static int sendcommand(FILE *, const char *, ...);

static int vflag = 0;	/* verbose */
static int nflag = 0;	/* no-delete */

/*
 * Opens a stream connection to the given host and port.
 */
static FILE *
openhostport(host, port)
	const char *host;
	const char *port;
{
	struct addrinfo hints, *res, *res0;
	int error;
	int s;
	const char *cause = NULL;
	FILE *conn;

	memset(&hints, 0, sizeof(hints));
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	error = getaddrinfo(host, port, &hints, &res0);
	if (error) 
		errx(1, "%s", gai_strerror(error));

	s = -1;
	for (res = res0; res; res = res->ai_next) {
		s = socket(res->ai_family, res->ai_socktype,
		    res->ai_protocol);
		if (s < 0) {
			cause = "socket";
			continue;
		}
		if (connect(s, res->ai_addr, res->ai_addrlen) < 0) {
			cause = "connect";
			close(s);
			s = -1;
			continue;
		}
		break; /* connected */
	}
	if (s == -1)
		err(1, cause);
	freeaddrinfo(res0);

	conn = fdopen(s, "w+b");
	if (!conn)
		err(1, "fdopen");
	return conn;
}

/*
 * Reads and returns the first newline-terminated line from a file,
 * in storage allocated using malloc(). If the given filename is NULL,
 * or a hyphen, then standard input is assumed.
 */
static char *
readpasswd(passwdfile)
	const char *passwdfile;
{
	FILE *pf;
	char *line;
	char *passwd;
	size_t size;

	if (!passwdfile || strcmp(passwdfile, "-") == 0) {
		pf = stdin;
		passwdfile = "<stdin>";
	} else {
		pf = fopen(passwdfile, "rb");
		if (pf == NULL)
			err(1, "%s", passwdfile);
	}

	/* Read the first line of the file */
	if (vflag)
		fprintf(stderr, "obtaining password from %s\n", 
		    passwdfile);

	if (isatty(fileno(pf))) {
		line = getpass("Password: ");
		if (!line)
			err(1, "%s: getpass", passwdfile);
		size = strlen(line);
	} else {
		line = fgetln(pf, &size);
		if (!line)
			err(1, "%s: read", passwdfile);
		if (size && line[size - 1] == '\n')
			size--;
	}
	passwd = malloc(size + 1);
	memcpy(passwd, line, size);
	passwd[size] = '\0';

	return passwd;
}

static char *response_text;
static int response_size;

/*
 * Reads a line of response from the connection.
 * It sets the response_text variable to point to either 
 *  (a) static storage holding the text following the +OK/-ERR indicator, or
 *  (b) the result of strerror() if the connection was closed
 * The response_size text indicates the length of the response, since
 * the text does not end in a nul character.
 *
 * Returns:
 *   1 on success (+OK)
 *   0 on failure (-ERR or eof/error)
 */
static int
getresponse(conn)
	FILE *conn;
{
	char *line;
	size_t size;
	int ret;

	line = fgetln(conn, &size);
	if (line == NULL) {
		response_text = strerror(ferror(conn));
		response_size = strlen(response_text);
		clearerr(conn);
		return 0;
	}

	/* Trim off trailing CRLF */
	if (size > 0 && line[size - 1] == LF)
		size--;
	if (size > 0 && line[size - 1] == CR)
		size--;

	if (vflag)
		fprintf(stderr, "-> %.*s\n", (int)size, line);

	if (size >= 3 && memcmp(line, "+OK", 3) == 0) {
		size -= 3;
		line += 3;
		ret = 1;
	} else if (size >= 4 && memcmp(line, "-ERR", 4) == 0) {
		size -= 4;
		line += 4;
		ret = 0;
	} else
		ret = 0;

	if (size > 0 && line[0] == ' ') {
		size--;
		line++;
	}

	response_text = line;
	response_size = size;
	return ret;
}

static char *line_text;
static int line_size;

/*
 * Reads a line of text from the connection, stripping the CRLF,
 * and removing any leading period. The result is placed in line_text
 * and the length in line_size.
 * Returns:
 *     1 on a normal line (with leading terminator removed if needed)
 *     0 on a sole terminator line
 *    -1 on error
 */

static int
getline(conn)
	FILE *conn;
{
	char *line;
	size_t size;

	line = fgetln(conn, &size);
	if (line == NULL) {
		line_text = strerror(ferror(conn));
		line_size = strlen(line_text);
		clearerr(conn);
		return -1;
	}

	/* Trim off trailing CRLF */
	if (size > 0 && line[size - 1] == LF)
		size--;
	if (size > 0 && line[size - 1] == CR)
		size--;

	if (size > 0 && line[0] == '.') {
		size--;
		line++;
		if (size == 0)
			return 0;
	}
	line_text = line;
	line_size = size;
	return 1;
}

static int
sendcommand(conn, fmt)
	FILE *conn;
	const char *fmt;
{
	va_list ap;
	int ret;

	va_start(ap, fmt);
	vfprintf(conn, fmt, ap);
	fputc(CR, conn);
	fputc(LF, conn);
	va_end(ap);

	if (vflag) {
		va_start(ap, fmt);
		fprintf(stderr, "<- ");
		if (memcmp(fmt, "PASS ", 5) == 0)
			fprintf(stderr, "PASS ????????");
		else
			vfprintf(stderr, fmt, ap);
		fprintf(stderr, "\n");
		va_end(ap);
	}

	ret = getresponse(conn);
	if (!ret)
		warnx("%.*s", response_size, response_text);
	return ret;
}

/*
 * Send USER and PASS commands to server to get from AUTHORIZATION
 * to AUTHENTICATED state.
 */
static int
do_userpass(conn, user, passwd, passwdfile)
	FILE *conn;
	const char *user;
	char *passwd;
	const char *passwdfile;
{
	int ret;

	if (!sendcommand(conn, "USER %s", user))
		return 0;

	/* Obtain password from a file if not supplied */
	if (!passwd) 
		passwd = readpasswd(passwdfile);
	ret = sendcommand(conn, "PASS %s", passwd);

	/* clear the password */
	memset(passwd, 'x', strlen(passwd));

	return ret;
}

/*
 * Send a STAT command to count the number of messages.
 */
static int
do_stat(conn, msgsp, sizep)
	FILE *conn;
	int *msgsp, *sizep;
{
	char *s;

	if (!sendcommand(conn, "STAT")) 
		return 0;

	s = response_text + response_size;
	*msgsp = strtoull(response_text, &s, 10);
	if (s < response_text + response_size && *s == ' ')
		s++;
	else {
		warnx("malformed STAT response");
		return 0;
	}

	*sizep = strtoull(s, &s, 10);
	return 1;
}

/*
 * Send the QUIT command and wait for the positive reponse.
 * Closes the connection on success.
 */
static int
do_quit(conn)
	FILE *conn;
{
	if (!sendcommand(conn, "QUIT"))
		return 0;
	fclose(conn);
	return 1;
}

/*
 * Send the RETR command that retreives a message, 
 * and write the body to stdout, using . terminators.
 * Returns 1 on success.
 */
static int
do_retr(conn, id)
	FILE *conn;
	int id;
{
	int done;
	int lines;

	if (!sendcommand(conn, "RETR %d", id))
		return 0;

	done = 0;
	lines = 0;
	while (!done) 
	    switch (getline(conn)) {
	    case 1:
		if (line_size > 0) {
			if (line_text[0] == '.')
				putchar('.');
			fwrite(line_text, line_size, 1, stdout);
		}
		putchar('\n');
		lines++;
		break;
	    case 0:
		done = 1;
		printf(".\n");
		break;
	    case -1:
		errx(1, "getline: %.*s", line_size, line_text);
		return 0;
	    }

	if (vflag)
		fprintf(stderr, "message %d: %d lines\n", id, lines);
	return 1;
}

/*
 * Send the DELE command that deletes a message
 */
static int
do_dele(conn, id)
	FILE *conn;
	int id;
{
	return sendcommand(conn, "DELE %d", id);
}

int
main(argc, argv)
	int argc;
	char *argv[];
{
	int ch;
	const char *user = NULL;
	const char *passwdfile = NULL;
	const char *host = NULL;
	const char *port = NULL;
	char *passwd = NULL;
	FILE *conn;
	int error = 0;
	int total_msgs, total_size;
	int id;

	/* Handle command-line options */
	while ((ch = getopt(argc, argv, "f:np:u:v")) != -1)
	    switch (ch) {
	    case 'f':
		passwdfile = optarg;
		passwd = NULL;
		break;
	    case 'n':
		nflag = 1;
		break;
	    case 'p':
		passwd = optarg;
		passwdfile = NULL;
		break;
	    case 'u':
		user = optarg;
		break;
	    case 'v':
		vflag = 1;
		break;
	    default:
		error = 1;
		break;
	    }


	if (optind < argc)
		host = argv[optind++];
	if (optind < argc)
		port = argv[optind++];

	/* Check that options were okay */
	if (host == NULL || optind < argc)
		error = 1;
	if (error) {
		fprintf(stderr, "usage: %s [-nv]"
				" [-u user] [-p pass|-f passfile]"
				" hostname [port]\n", argv[0]);
		exit(1);
	}

	/* Obtain a username if not supplied */
	if (!user) {
		user = getlogin();
		if (!user)
			err(1, "getlogin");
	}

	/* Connect to the pop server */
	if (!port)
		port = "pop3";
	if (vflag)
		fprintf(stderr, "connecting to %s:%s\n", host, port);
	conn = openhostport(host, port);

	/* Get server's greeting message */
	if (!getresponse(conn))
		errx(1, "%.*s", response_size, response_text);

	/* Log in */
	if (!do_userpass(conn, user, passwd, passwdfile))
		exit(1);

	/* Count messages */
	if (!do_stat(conn, &total_msgs, &total_size))
		exit(1);

	if (vflag)
		fprintf(stderr, "total_msgs = %d, total_size = %d\n", 
		    total_msgs, total_size);

	/* Retrieve all the messages to stdout */
	for (id = 1; id <= total_msgs; id++)
		if (!do_retr(conn, id))
			exit(1);

	/* Delete the messages that we got */
	if (!nflag)
		for (id = 1; id <= total_msgs; id++)
			if (!do_dele(conn, id))
				exit(1);

	/* Complete */
	if (!do_quit(conn))
		exit(1);

	exit(0);
}

