first stab at simple mailer

Simon 'corecode' Schubert corecode at fs.ei.tum.de
Tue Mar 20 14:48:03 PDT 2007


Hey,

again and again people are complaining about why sendmail is in base and why not postfix, etc.  We keep saying that we do need a mail delivery/transport agent, for stuff such as periodic, cron, etc.

But that doesn't mean that we need sendmail.  Actually a much simpler mailer would do:  one that just delivers locally (and if possible, remote) and does nothing else.

I've started writing such a mailer which can be used to replace sendmail for the most basic mail jobs.  I consider this in an early stage of development, but much of the functionality is already there.  Please review the code/concept and give me feedback.

How it works:
- binary will be setgid mail or such.
- mail gets accepted from stdin
- mail gets written to spool dir
- per recipient one child is being forked
- childs try delivering
- on timeout a bounce is created
- if the system goes down, /etc/rc would start the mailer in queue processing mode, which does the same like above except for reading the mail from stdin.  instead it reads the queue.
What is still missing:
- SMTP delivery
- alias expansion
- proper sysexit codes
What I am not sure about:
- should the mailer block until all recipients are served or should it (like now) daemonize instantly
comments welcome!  (and contributions as well)

latest development version is always at <http://ww2.fs.ei.tum.de/~corecode/git?p=dma.git>

attached current version.

cheers
 simon
--
Serve - BSD     +++  RENT this banner advert  +++    ASCII Ribbon   /"\
Work - Mac      +++  space for low €€€ NOW!1  +++      Campaign     \ /
Party Enjoy Relax   |   http://dragonflybsd.org      Against  HTML   \
Dude 2c 2 the max   !   http://golden-apple.biz       Mail + News   / \
#include <sys/param.h>
#include <sys/queue.h>
#include <sys/stat.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <paths.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#define VERSION	"DragonFly Mail Agent 0.1"

#define MIN_RETRY	300		/* 5 minutes */
#define MAX_RETRY	(3*60*60)	/* retry at least every 3 hours */
#define MAX_TIMEOUT	(5*24*60*60)	/* give up after 5 days */
#define SPOOL_PATH	"/tmp/spool"/*"/var/spool/mqueue"*/

struct qitem {
	LIST_ENTRY(qitem) next;
	const char *sender;
	char *addr;
	char *queuefn;
	char *queueid;
	FILE *queuef;
	off_t hdrlen;
	int remote;
};
LIST_HEAD(queueh, qitem);

struct queue {
	struct queueh queue;
	uintmax_t id;
	int mailfd;
	char *tmpf;
};


static void deliver(struct qitem *);

struct tmpf {
	SLIST_ENTRY(tmpf) next;
	char *name;
};


static SLIST_HEAD(, tmpf) tmpfs = SLIST_HEAD_INITIALIZER(tmpfs);
static int daemonize = 1;


static char *
hostname(void)
{
	static char name[MAXHOSTNAMELEN+1];

	if (gethostname(name, sizeof(name)) != 0)
		strcpy(name, "(unknown hostname)");

	return name;
}

static char *
set_from(const char *osender)
{
	char *sender;

	if (osender) {
		sender = strdup(osender);
		if (sender == NULL)
			return (NULL);
	} else {
		if (asprintf(&sender, "%s@%s", getlogin(), hostname()) <= 0)
			return (NULL);
	}

	if (strchr(sender, '\n') != NULL) {
		errno = EINVAL;
		return (NULL);
	}

	return (sender);
}

static int
add_recp(struct queue *queue, const char *str, const char *sender, int expand)
{
	struct qitem *it, *tit;
	struct passwd *pw;
	char *host;

#if 0
	if (expand)
		/* XXX add alias processing */
#endif

	it = calloc(1, sizeof(*it));
	if (it == NULL)
		return (-1);
	it->addr = strdup(str);
	if (it->addr == NULL)
		return (-1);

	it->sender = sender;
	if ((host = strrchr(it->addr, '@')) != NULL) {
		/* Remote address */
		if (strcmp(host + 1, hostname()) == 0 ||
		    strcmp(host + 1, "localhost") == 0) {
			/* It is really a local thingy */
			*host = 0;
			goto local;
		}
		/* XXX validate address/host? */
		it->remote = 1;
	} else {
local:
		/* Local destination, check */
		pw = getpwnam(it->addr);
		if (expand && pw == NULL)
			goto out;
		it->remote = 0;
		endpwent();
	}
	LIST_FOREACH(tit, &queue->queue, next) {
		/* weed out duplicate dests */
		if (strcmp(tit->addr, it->addr) == 0) {
			free(it->addr);
			free(it);
			return (0);
		}
	}
	LIST_INSERT_HEAD(&queue->queue, it, next);
	return (0);

out:
	free(it->addr);
	free(it);
	return (-1);
}

static void
deltmp(void)
{
	struct tmpf *t;

	SLIST_FOREACH(t, &tmpfs, next) {
		unlink(t->name);
	}
}

static int
gentempf(struct queue *queue)
{
	char fn[PATH_MAX+1];
	struct tmpf *t;
	int fd;

	if (snprintf(fn, sizeof(fn), "%s/%s", SPOOL_PATH, "tmp_XXXXXXXXXX") <= 0)
		return (-1);
	fd = mkstemp(fn);
	if (fd < 0)
		return (-1);
	queue->mailfd = fd;
	queue->tmpf = strdup(fn);
	if (queue->tmpf == NULL) {
		unlink(fn);
		return (-1);
	}
	t = malloc(sizeof(*t));
	if (t != NULL) {
		t->name = queue->tmpf;
		SLIST_INSERT_HEAD(&tmpfs, t, next);
	}
	return (0);
}

/*
 * spool file format:
 *
 * envelope-from
 * queue-id1 envelope-to1
 * queue-id2 envelope-to2
 * ...
 * <empty line>
 * mail data
 *
 * queue ids are unique, formed from the inode of the spool file
 * and a unique identifier.
 */
static int
preparespool(struct queue *queue, const char *sender)
{
	char line[1000];	/* by RFC2822 */
	struct stat st;
	int error;
	struct qitem *it;
	FILE *queuef;
	off_t hdrlen;

	error = snprintf(line, sizeof(line), "%s\n", sender);
	if (error < 0 || (size_t)error >= sizeof(line)) {
		errno = E2BIG;
		return (-1);
	}
	if (write(queue->mailfd, line, error) != error)
		return (-1);

	queuef = fdopen(queue->mailfd, "r+");
	if (queuef == NULL)
		return (-1);

	/*
	 * Assign queue id to each dest.
	 */
	if (fstat(queue->mailfd, &st) != 0)
		return (-1);
	queue->id = st.st_ino;
	LIST_FOREACH(it, &queue->queue, next) {
		if (asprintf(&it->queueid, "%"PRIxMAX".%"PRIxPTR,
			     queue->id, (uintptr_t)it) <= 0)
			return (-1);
		if (asprintf(&it->queuefn, "%s/%s",
			     SPOOL_PATH, it->queueid) <= 0)
			return (-1);
		/* File may not exist yet */
		if (stat(it->queuefn, &st) == 0)
			return (-1);
		it->queuef = queuef;
		error = snprintf(line, sizeof(line), "%s %s\n",
			       it->queueid, it->addr);
		if (error < 0 || (size_t)error >= sizeof(line))
			return (-1);
		if (write(queue->mailfd, line, error) != error)
			return (-1);
	}
	line[0] = '\n';
	if (write(queue->mailfd, line, 1) != 1)
		return (-1);

	hdrlen = lseek(queue->mailfd, 0, SEEK_CUR);
	LIST_FOREACH(it, &queue->queue, next) {
		it->hdrlen = hdrlen;
	}
	return (0);
}

static char *
rfc822date(void)
{
	static char str[50];
	size_t error;
	time_t now;

	now = time(NULL);
	error = strftime(str, sizeof(str), "%a, %d %b %Y %T %z",
		       localtime(&now));
	if (error == 0)
		strcpy(str, "(date fail)");
	return (str);
}

static int
readmail(struct queue *queue, const char *sender, int nodot)
{
	char line[1000];	/* by RFC2822 */
	size_t linelen;
	int error;

	error = snprintf(line, sizeof(line), "\
Received: from %s (uid %d)\n\
\t(envelope-from %s)\n\
\tid %"PRIxMAX"\n\
\tby %s (%s);\n\
\t%s\n",
		getlogin(), getuid(),
		sender,
		queue->id,
		hostname(), VERSION,
		rfc822date());
	if (error < 0 || (size_t)error >= sizeof(line))
		return (-1);
	if (write(queue->mailfd, line, error) != error)
		return (-1);

	while (!feof(stdin)) {
		if (fgets(line, sizeof(line), stdin) == NULL)
			break;
		linelen = strlen(line);
		if (linelen == 0 || line[linelen - 1] != '\n') {
			errno = EINVAL;		/* XXX mark permanent errors */
			return (-1);
		}
		if (!nodot && linelen == 2 && line[0] == '.')
			break;
		if ((size_t)write(queue->mailfd, line, linelen) != linelen)
			return (-1);
	}
	if (fsync(queue->mailfd) != 0)
		return (-1);
	return (0);
}

static int
linkspool(struct queue *queue)
{
	struct qitem *it;

	LIST_FOREACH(it, &queue->queue, next) {
		if (link(queue->tmpf, it->queuefn) != 0)
			goto delfiles;
	}
	unlink(queue->tmpf);
	return (0);

delfiles:
	LIST_FOREACH(it, &queue->queue, next) {
		unlink(it->queuefn);
	}
	return (-1);
}

static struct qitem *
go_background(struct queue *queue)
{
	struct sigaction sa;
	struct qitem *it;
	pid_t pid;

	if (daemonize && daemon(0, 0) != 0) {
		syslog(LOG_ERR, "can not daemonize: %m");
		exit(1);
	}
	daemonize = 0;

	bzero(&sa, sizeof(sa));
	sa.sa_flags = SA_NOCLDWAIT;
	sa.sa_handler = SIG_IGN;
	sigaction(SIGCHLD, &sa, NULL);

	LIST_FOREACH(it, &queue->queue, next) {
		/* No need to fork for the last dest */
		if (LIST_NEXT(it, next) == NULL)
			return (it);

		pid = fork();
		switch (pid) {
		case -1:
			syslog(LOG_ERR, "can not fork: %m");
			exit(1);
			break;

		case 0:
			/*
			 * Child:
			 *
			 * return and deliver mail
			 */
			return (it);

		default:
			/*
			 * Parent:
			 *
			 * fork next child
			 */
			break;
		}
	}

	syslog(LOG_CRIT, "reached dead code");
	exit(1);
}

static void
bounce(struct qitem *it, const char *reason)
{
	struct queue bounceq;
	struct qitem *bit;
	char line[1000];
	int error;

	/* Don't bounce bounced mails */
	if (it->sender[0] == 0) {
		syslog(LOG_CRIT, "%s: delivery panic: can't bounce a bounce",
		       it->queueid);
		exit(1);
	}

	syslog(LOG_ERR, "%s: delivery failed, bouncing",
	       it->queueid);

	LIST_INIT(&bounceq.queue);
	if (add_recp(&bounceq, it->sender, "", 1) != 0)
		goto fail;
	if (gentempf(&bounceq) != 0)
		goto fail;
	if (preparespool(&bounceq, "") != 0)
		goto fail;

	bit = LIST_FIRST(&bounceq.queue);
	error = fprintf(bit->queuef, "\
Received: from MAILER-DAEMON\n\
\tid %"PRIxMAX"\n\
\tby %s (%s);\n\
\t%s\n\
X-Original-To: <%s>\n\
From: MAILER-DAEMON <>\n\
To: %s\n\
Subject: Mail delivery failed\n\
Message-Id: <%"PRIxMAX"@%s>\n\
Date: %s\n\
\n\
This is the %s at %s.\n\
\n\
There was an error delivering your mail to <%s>.\n\
\n\
%s\n\
\n\
Message headers follow.\n\
\n\
",
		bounceq.id,
		hostname(), VERSION,
		rfc822date(),
		it->addr,
		it->sender,
		bounceq.id, hostname(),
		rfc822date(),
		VERSION, hostname(),
		it->addr,
		reason);
	if (error < 0)
		goto fail;
	if (fflush(bit->queuef) != 0)
		goto fail;

	if (fseek(it->queuef, it->hdrlen, SEEK_SET) != 0)
		goto fail;
	while (!feof(it->queuef)) {
		if (fgets(line, sizeof(line), it->queuef) == NULL)
			break;
		if (line[0] == '\n')
			break;
		write(bounceq.mailfd, line, strlen(line));
	}
	if (fsync(bounceq.mailfd) != 0)
		goto fail;
	if (linkspool(&bounceq) != 0)
		goto fail;
	/* bounce is safe */

	unlink(it->queuefn);
	fclose(it->queuef);

	bit = go_background(&bounceq);
	deliver(bit);
	/* NOTREACHED */

fail:
	syslog(LOG_CRIT, "%s: error creating bounce: %m", it->queueid);
	unlink(it->queuefn);
	exit(1);
}

static int
deliver_local(struct qitem *it, const char **errmsg)
{
	char fn[PATH_MAX+1];
	char line[1000];
	size_t linelen;
	int mbox;
	int error;
	off_t mboxlen;
	time_t now = time(NULL);

	error = snprintf(fn, sizeof(fn), "%s/%s", _PATH_MAILDIR, it->addr);
	if (error < 0 || (size_t)error >= sizeof(fn)) {
		syslog(LOG_ERR, "%s: local delivery deferred: %m",
		       it->queueid);
		return (1);
	}
	mbox = open(fn, O_WRONLY|O_EXLOCK|O_APPEND);
	if (mbox < 0) {
		syslog(LOG_ERR, "%s: local delivery deferred: can not open `%s': %m",
		       it->queueid, fn);
		return (1);
	}
	mboxlen = lseek(mbox, 0, SEEK_CUR);

	if (fseek(it->queuef, it->hdrlen, SEEK_SET) != 0) {
		syslog(LOG_ERR, "%s: local delivery deferred: can not seek: %m",
		       it->queueid);
		return (1);
	}

	error = snprintf(line, sizeof(line), "From %s\t%s", it->sender, ctime(&now));
	if (error < 0 || (size_t)error >= sizeof(line)) {
		syslog(LOG_ERR, "%s: local delivery deferred: can not write header: %m",
		       it->queueid);
		return (1);
	}
	if (write(mbox, line, error) != error)
		goto wrerror;

	while (!feof(it->queuef)) {
		if (fgets(line, sizeof(line), it->queuef) == NULL)
			break;
		linelen = strlen(line);
		if (linelen == 0 || line[linelen - 1] != '\n') {
			syslog(LOG_CRIT, "%s: local delivery failed: corrupted queue file",
			       it->queueid);
			*errmsg = "corrupted queue file";
			error = -1;
			goto chop;
		}

		if (strncmp(line, "From ", 5) == 0) {
			const char *gt = ">";

			if (write(mbox, gt, 1) != 1)
				goto wrerror;
		}
		if ((size_t)write(mbox, line, linelen) != linelen)
			goto wrerror;
	}
	line[0] = '\n';
	if (write(mbox, line, 1) != 1)
		goto wrerror;
	close(mbox);
	return (0);

wrerror:
	syslog(LOG_ERR, "%s: local delivery failed: write error: %m",
	       it->queueid);
	error = 1;
chop:
	if (ftruncate(mbox, mboxlen) != 0)
		syslog(LOG_WARNING, "%s: error recovering mbox `%s': %m",
		       it->queueid, fn);
	close(mbox);
	return (error);
}

static void
deliver(struct qitem *it)
{
	int error;
	unsigned int backoff = MIN_RETRY;
	const char *errmsg = "unknown bounce reason";
	struct timeval now;
	struct stat st;

	syslog(LOG_INFO, "%s: mail from=<%s> to=<%s>",
	       it->queueid, it->sender, it->addr);

retry:
	syslog(LOG_INFO, "%s: trying delivery",
	       it->queueid);

	if (it->remote) {
		errmsg = "remote delivery not implemented";
		error = -1;/* XXX implement me */
	} else
		error = deliver_local(it, &errmsg);

	switch (error) {
	case 0:
		unlink(it->queuefn);
		syslog(LOG_INFO, "%s: delivery successful",
		       it->queueid);
		exit(0);

	case 1:
		if (stat(it->queuefn, &st) != 0) {
			syslog(LOG_ERR, "%s: lost queue file `%s'",
			       it->queueid, it->queuefn);
			exit(1);
		}
		if (gettimeofday(&now, NULL) == 0 &&
		    (now.tv_sec - st.st_mtimespec.tv_sec > MAX_TIMEOUT)) {
			char *msg;

			if (asprintf(&msg,
				 "Could not deliver for the last %d seconds. Giving up.",
				 MAX_TIMEOUT) > 0)
				errmsg = msg;
			goto bounce;
		}
		sleep(backoff);
		backoff *= 2;
		if (backoff > MAX_RETRY)
			backoff = MAX_RETRY;
		goto retry;

	case -1:
	default:
		break;
	}

bounce:
	bounce(it, errmsg);
	/* NOTREACHED */
}

static void
run_queue(void)
{
	struct stat st;
	struct qitem *it;
	struct queue queue, itmqueue;
	DIR *spooldir;
	struct dirent *de;
	char line[1000];
	char *fn;
	FILE *queuef;
	char *sender;
	char *addr;
	char *queueid;
	char *queuefn;
	off_t hdrlen;
	int fd;

	LIST_INIT(&queue.queue);

	spooldir = opendir(SPOOL_PATH);
	if (spooldir == NULL)
		err(1, "reading queue");

	while ((de = readdir(spooldir)) != NULL) {
		sender = NULL;
		queuef = NULL;
		queueid = NULL;
		queuefn = NULL;
		fn = NULL;
		LIST_INIT(&itmqueue.queue);

		/* ignore temp files */
		if (strncmp(de->d_name, "tmp_", 4) == 0 ||
		    de->d_type != DT_REG)
			continue;
		if (asprintf(&queuefn, "%s/%s", SPOOL_PATH, de->d_name) < 0)
			goto fail;
		fd = open(queuefn, O_RDONLY|O_EXLOCK|O_NONBLOCK);
		if (fd < 0) {
			/* Ignore locked files */
			if (errno == EWOULDBLOCK)
				continue;
			goto skip_item;
		}

		queuef = fdopen(fd, "r");
		if (queuef == NULL)
			goto skip_item;
		if (fgets(line, sizeof(line), queuef) == NULL ||
		    line[0] == 0)
			goto skip_item;
		line[strlen(line) - 1] = 0;	/* chop newline */
		sender = strdup(line);
		if (sender == NULL)
			goto skip_item;

		for (;;) {
			if (fgets(line, sizeof(line), queuef) == NULL ||
			    line[0] == 0)
				goto skip_item;
			if (line[0] == '\n')
				break;
			line[strlen(line) - 1] = 0;
			queueid = strdup(line);
			if (queueid == NULL)
				goto skip_item;
			addr = strchr(queueid, ' ');
			if (addr == NULL)
				goto skip_item;
			*addr++ = 0;
			if (fn != NULL)
				free(fn);
			if (asprintf(&fn, "%s/%s", SPOOL_PATH, queueid) < 0)
				goto skip_item;
			/* Item has already been delivered? */
			if (stat(fn, &st) != 0)
				continue;
			if (add_recp(&itmqueue, addr, sender, 0) != 0)
				goto skip_item;
			it = LIST_FIRST(&itmqueue.queue);
			it->queuef = queuef;
			it->queueid = queueid;
			it->queuefn = fn;
			fn = NULL;
		}
		if (LIST_EMPTY(&itmqueue.queue)) {
			warnx("queue file without items: `%s'", queuefn);
			goto skip_item2;
		}
		hdrlen = ftell(queuef);
		while ((it = LIST_FIRST(&itmqueue.queue)) != NULL) {
			it->hdrlen = hdrlen;
			LIST_REMOVE(it, next);
			LIST_INSERT_HEAD(&queue.queue, it, next);
		}
		continue;

skip_item:
		warn("reading queue: `%s'", queuefn);
skip_item2:
		if (sender != NULL)
			free(sender);
		if (queuefn != NULL)
			free(queuefn);
		if (fn != NULL)
			free(fn);
		if (queueid != NULL)
			free(queueid);
		close(fd);
	}
	closedir(spooldir);

	it = go_background(&queue);
	deliver(it);
	/* NOTREACHED */

fail:
	err(1, "reading queue");
}

/*
 * TODO:
 *
 * - SMTP delivery
 * - alias processing
 * - use group permissions
 * - proper sysexit codes
 */

int
main(int argc, char **argv)
{
	char *sender = NULL;
	struct qitem *it;
	struct queue queue;
	int i, ch;
	int nodot = 0, doqueue = 0;

	atexit(deltmp);
	LIST_INIT(&queue.queue);
	openlog("dma", LOG_PID|LOG_PERROR, LOG_MAIL);

	while ((ch = getopt(argc, argv, "Df:io:qr:")) != -1) {
		switch (ch) {
		case 'D':
			daemonize = 0;
			break;

		case 'f':
		case 'r':
			sender = optarg;
			break;

		case 'o':
			/* -oX is being ignored, except for -oi */
			if (optarg[0] != 'i')
				break;
			/* else FALLTRHOUGH */
		case 'i':
			nodot = 1;
			break;

		case 'q':
			doqueue = 1;
			break;

		default:
			exit(1);
		}
	}
	argc -= optind;
	argv += optind;

	if (doqueue) {
		if (argc != 0)
			errx(1, "sending mail and queue pickup is mutually exclusive");
		run_queue();
		/* NOTREACHED */
	}

	if ((sender = set_from(sender)) == NULL)
		err(1, "setting from address");

	for (i = 0; i < argc; i++) {
		if (add_recp(&queue, argv[i], sender, 1) != 0)
			errx(1, "invalid recipient `%s'\n", argv[i]);
	}

	if (LIST_EMPTY(&queue.queue))
		errx(1, "no recipients");

	if (gentempf(&queue) != 0)
		err(1, "create temp file");

	if (preparespool(&queue, sender) != 0)
		err(1, "creating spools (1)");

	if (readmail(&queue, sender, nodot) != 0)
		err(1, "reading mail");

	if (linkspool(&queue) != 0)
		err(1, "creating spools (2)");

	/* From here on the mail is safe. */

	it = go_background(&queue);
	deliver(it);

	/* NOTREACHED */

	return (0);
}
Attachment:
signature.asc
-------------- next part --------------
A non-text attachment was scrubbed...
Name: pgp00009.pgp
Type: application/octet-stream
Size: 252 bytes
Desc: "Description: OpenPGP digital signature"
URL: <http://lists.dragonflybsd.org/pipermail/kernel/attachments/20070320/dd25548e/attachment-0019.obj>


More information about the Kernel mailing list