/*
 * $Id: 4e830769bd67082087fda8b1592f50ec13bb5463 $
 *
 * This file is part of nsmtp.
 *
 * Copyright (c) 2007, 2008 Niklas Olmes <niklas@noxa.de>
 *                          http://www.nevm.de/nsmtp/
 *                          NEVM-RIPE
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License Version 2
 * as published by the Free Software Foundation.
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#ifdef USE_MMAP
#include <sys/mman.h>
#endif
#include <errno.h>
#include <dirent.h>
#include <assert.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "alloc.h"
#include "smtp.h"
#include "log.h"
#include "ndn.h"
#include "spool.h"
#include "config.h"

#define RECEIVED_LINE	"Received:"
#define DELIVERED_LINE	"Delivered-To:"
#define MAX_RECEIVED	60
#define MAX_DELIVERED	30

static const char rcsid[] = "$Id: 4e830769bd67082087fda8b1592f50ec13bb5463 $";

static int detect_mailloop(const SMTP_CTX *);

SMTP_CTX *
read_spoolfile(const char *subdir, const char *dirfn)
{
	SMTP_CTX *ctx;
	FILE *fp;
	char buf[65532];
	char fn[256];
#ifndef USE_MMAP
	size_t total, len;
#else
	off_t offset;
#endif
	int had_date, had_mid, had_from, had_to, had_defer, had_connid;

	(void)snprintf(fn, sizeof(fn), "%s/%s/%s", nsmc_spool_root, subdir, dirfn);

	DEBUG(("read_spoolfile() => spoolfile <%s>", fn));

	if ((fp = fopen(fn, "r+")) == 0) {
		xwarn("warn: read_spoolfile() => unable to read spoolfile <%s>: \"%s\"", fn, strerror(errno));
		return NULL;
	}

	ctx = init_smtp_ctx();

	had_date = had_mid = had_to = had_defer = had_from = had_connid = 0;
	while (fgets(buf, sizeof(buf), fp) != NULL) {
		if (*buf == '\n' || *buf == '-')
			goto out;
		switch(*buf) {
		case 'F': /* FALLTHRU */
		case 'E':
			if (had_from) {
				xwarn("warn: read_spoolfile() => spoofile <%s> has duplicate E/F header", fn);
				goto err_out;
			}
			if (*buf == 'E')
				ctx->exploded = 1;
			buf[strlen(buf) - 1] = '\0';
			ctx->from = nsm_strdup(buf + 1);
			had_from++;
			break;
		case 'T':
			if (had_date) {
				xwarn("warn: read_spoolfile() => spoofile <%s> has duplicate T header", fn);
				goto err_out;
			}
			ctx->spooltime = atol(buf + 1);
			had_date++;
			break;
		case 'M':
			if (had_mid) {
				xwarn("warn: read_spoolfile() => spoolfile <%s> has duplicate M header", fn);
				goto err_out;
			}
			buf[strlen(buf) - 1] = '\0';
			ctx->lmid = nsm_strdup(buf + 1);
			had_mid++;
			break;
		case 'R':
			if (ctx->rcpts == NULL)
				ctx->rcpts = nsm_calloc(2, sizeof(char *));
			else
				ctx->rcpts = nsm_realloc(ctx->rcpts, (had_to + 2) * sizeof(char *));
			buf[strlen(buf) - 1] = '\0';
			ctx->rcpts[had_to] = nsm_strdup(buf + 1);
			ctx->rcpts[had_to + 1] = NULL;
			had_to++;
			break;
		case 'D':
			if (ctx->defers == NULL)
				ctx->defers = nsm_calloc(2, sizeof(char *));
			else
				ctx->defers = nsm_realloc(ctx->defers, (had_defer + 2) * sizeof(char *));
			buf[strlen(buf) - 1] = '\0';
			ctx->defers[had_defer] = nsm_strdup(buf + 1);
			ctx->defers[had_defer + 1] = NULL;
			had_defer++;
			break;
		case 'S':
			if (had_connid) {
				xwarn("warn: read_spoolfile() => spoolfile <%s> has duplicate S header", fn);
				goto err_out;
			}
			buf[strlen(buf) - 1] = '\0';
			ctx->conn_id = nsm_strdup(buf + 1);
			had_connid++;
			break;
		case '#':
			/* ignore */
			break;
		default:
			xwarn("warn: read_spoolfile() => spoofile <%s> has unknown header field '%c', fatal", fn, *buf);
			goto err_out;
		}
	}

err_out:
	xwarn("warn: read_spoolfile() => spoofile <%s> is defective", fn);
	destroy_smtp_ctx(ctx);
	(void)fclose(fp);
	return NULL;

out:
	if (!had_date || !had_mid || (!had_to && !had_defer) || !had_from || !had_connid) {
		xwarn("warn: read_spoolfile() => spoolfile <%s> is defective, missing attribs", fn);
		destroy_smtp_ctx(ctx);
		(void)fclose(fp);
		return NULL;
	}

#ifndef USE_MMAP
	/*
	 * read data part
	 */
	total = 0;
	ctx->data = nsm_calloc(1, sizeof(char));
	while ((len = fread(buf, 1, sizeof(buf), fp)) != 0) {
		ctx->data = nsm_realloc(ctx->data, (total + len) * sizeof(char));
		(void)memcpy(ctx->data + total, buf, (size_t)len);
		total += len;
	}

	if (!feof(fp)) {
		xwarn("warn: read_spoolfile() => while reading spoofile <%s>, EOF not reached", fn);
		destroy_smtp_ctx(ctx);
		(void)fclose(fp);
		return NULL;
	}

	ctx->size = total;
	(void)fclose(fp);
#else
	/*
	 * mmap() data portion of file
	 */
	(void)fclose(fp);

	if ((ctx->mmapfd = open(fn, O_RDONLY, 0)) < 0) {
		xwarn("warn: read_spoolfile() => unable to open(\"%s\", O_RDONLY): \"%s\"", fn, strerror(errno));
		destroy_smtp_ctx(ctx);
		return NULL;
	}
	if ((ctx->mmap = mmap(0, MAX_SIZE, PROT_READ, MAP_PRIVATE, ctx->mmapfd, 0)) == MAP_FAILED) {
		xwarn("warn: read_spoolfile() => mmap(\"%s\") failed", fn);
		destroy_smtp_ctx(ctx);
	}

	/*
	 * go after \n-\n
	 */
	ctx->data = strstr(ctx->mmap, "\n-\n") + 3;

	/*
	 * measure lenght of data
	 */
	offset = lseek(ctx->mmapfd, 0, SEEK_END);
	(void)lseek(ctx->mmapfd, 0, SEEK_SET);
	ctx->size = offset - (ctx->data - (char *)ctx->mmap);

	DEBUG(("read_spoolfile() => measured mmap()ed data size (%ld) bytes", (long)ctx->size));
#endif

	return ctx;
}

/*
 * return codes
 * 1 - yep, damnit, "oh noes!'
 * 0 - i think we're good, er, i mean: i hope so
 */
static int
detect_mailloop(const SMTP_CTX *ctx)
{
	int rcount, dcount;
	char *p;

	if (ctx->size < 1000L) /* don't bother */
		return 0;

	/*
	 * So, general idea:
	 * We count number of Received: and Delivered-To: headers.
	 * If they exceed a certain threshold, we assume we're stuck in a mail loop.
	 */

	#define NL()	if ((p = strchr(p, '\n')) == NULL) \
				break; \
			++p; \
			if (*p == '\n') \
				break;

	rcount = dcount = 0;
	p = ctx->data;

	for (;;) {
		if (strncasecmp(p, RECEIVED_LINE, strlen(RECEIVED_LINE)) == 0)
			rcount++;
		else if (strncasecmp(p, DELIVERED_LINE, strlen(DELIVERED_LINE)) == 0)
			dcount++;

		if (rcount > MAX_RECEIVED) {
			xfatal("{%s} fatal: security violation: detect_mailloop() detected mail loop via \"Received:\" headers, count (%d) > (%d)", ctx->conn_id, rcount, MAX_RECEIVED);
			return 1;
		}
		if (dcount > MAX_DELIVERED) {
			xfatal("{%s} fatal: security violation: detect_mailloop() detected mail loop via \"Delivered-To:\" headers, count (%d) > (%d)", ctx->conn_id, dcount, MAX_DELIVERED);
			return 1;
		}

		NL();
	}

	return 0;
}

int
spool(const char *subdir, const SMTP_CTX *ctx)
{
	char fn[256], temp[256], final[256], buf[1024], shortlmid[64];
	char *t822, *p;
	mode_t oldmask;
	size_t pos;
	int flag, has_date, has_mid, has_from, has_to;
	int fd;
	int i;

	if (detect_mailloop(ctx))
		return 13;

	/*
	 * (0) Create spool file.
	 */
	(void)snprintf(fn, sizeof(fn), "%lu.%lu,%u,%u:%s", (unsigned long)time(NULL), random()%100000 + 1, getpid(), getppid(), ctx->server);
	DEBUG(("spool() => unique fn is <%s>", fn));

	(void)snprintf(temp, sizeof(temp), "%s/temp/%s", nsmc_spool_root, fn);
	(void)snprintf(final, sizeof(final), "%s/%s/%s", nsmc_spool_root, subdir, fn);

	oldmask = umask(0002);
	if ((fd = open(temp, O_WRONLY | O_CREAT, 0660)) < 0) {
		xfatal("{%s} fatal: spool() => unable to create temporary spool file <%s>", ctx->conn_id, temp);
		(void)umask(oldmask);
		return 1;
	}
	(void)umask(oldmask);

	/*
	 * (0)(a) chown of spoolfile if we're root (i.e., smtpl using spool() for forwards)
	 */
	if (getuid() == 0)
		if (chown(temp, nsmc_smtps_uid, nsmc_smtps_gid) != 0) {
			xfatal("{%s} fatal: spool() => chown(%d, %d) failed for temporary spool file <%s>: \"%s\"", ctx->conn_id, nsmc_smtps_uid, nsmc_smtps_gid, temp, strerror(errno));
			(void)close(fd);
			(void)unlink(temp);
			return 1;
		}

	t822 = make_822time(time(NULL));

	#define WOD(len)	if (write(fd, buf, len) != (ssize_t)len) {	\
					(void)close(fd);			\
					goto error_out;				\
				}

	/*
	 * (1) Write our header.
	 */
	(void)snprintf(buf, sizeof(buf), "F%s\nM%s\nT%lu\nS%s\n", ctx->from, ctx->lmid, (unsigned long)time(NULL), ctx->conn_id);
	WOD(strlen(buf));

	for (i = 0; ctx->rcpts[i] != NULL; i++) {
		(void)snprintf(buf, sizeof(buf), "R%s\n", ctx->rcpts[i]);
		WOD(strlen(buf));
	}

	if (write(fd, "-\n", 2) != 2) {
		(void)close(fd);
		goto error_out;
	}

	/*
	 * (2) Add our received and spf lines.
	 */
	if (ctx->spfinfo != NULL) {
		(void)snprintf(buf, sizeof(buf), "Received-SPF: %s\n", ctx->spfinfo);
		WOD(strlen(buf));
	}

	if (ctx->recvdinfo != NULL)
		(void)snprintf(buf, sizeof(buf), "Received: %s; %s\n", ctx->recvdinfo, t822);
	else {
		(void)snprintf(shortlmid, sizeof(shortlmid), "%s", ctx->lmid);
		if ((p = strchr(shortlmid, '@')) != NULL)
			*p = '\0';
		(void)snprintf(buf, sizeof(buf),
		    "Received: from %s (%s [%s])\n"
		    "%s%s%s"
		    "\t(envelope-sender <%s>)\n"
		    "%s%s%s"
		    "\tby %s (nsmtp) with %s\n"
		    "\tid %s; %s\n",
		    ctx->helo == NULL ? ctx->client_ip : ctx->helo, *ctx->client_rn == '\0' ? "unknown" : ctx->client_rn, ctx->client_ip,
		    ctx->sslinfo != NULL ? "\t(" : "", ctx->sslinfo != NULL ? ctx->sslinfo : "", ctx->sslinfo != NULL ? ")\n" : "",
		    ctx->from,
		    ctx->authinfo != NULL ? "\t(" : "", ctx->authinfo != NULL ? ctx->authinfo : "", ctx->authinfo != NULL ? ")\n" : "",
		    ctx->server, ctx->esmtp ? "ESMTP" : "SMTP",
		    shortlmid, t822);
	}
	WOD(strlen(buf));

	/*
	 * (3) Add missing fields (Date:, Message-Id:, From:, To:), if any.
	 */
	flag = 0;
	has_date = has_mid = has_to = has_from = 0;
	for (pos = 0; pos < ctx->size; pos++) {
		/* break out if we get out of header */
		if ((pos == 0 && *ctx->data == '\n')
		    || (pos >= 1 && ctx->data[pos-1] == '\n' && ctx->data[pos] == '\n')
		    || (pos >= 3 && ctx->data[pos-3] == '\r' && ctx->data[pos-2] == '\n' && ctx->data[pos-1] == '\r' && ctx->data[pos] == '\n')) {
			flag = 1;
			break;
		}
		if (strncasecmp(ctx->data + pos, "Date: ", strlen("Date: ")) == 0)
			has_date++;
		else if (strncasecmp(ctx->data + pos, "Message-Id: ", strlen("Message-Id: ")) == 0)
			has_mid++;
		else if (strncasecmp(ctx->data + pos, "To: ", strlen("To: ")) == 0)
			has_to++;
		else if (strncasecmp(ctx->data + pos, "From: ", strlen("From: ")) == 0)
			has_from++;
	}

	if (!has_mid) {
		(void)snprintf(buf, sizeof(buf), "Message-Id: <%s>\n", ctx->lmid);
		WOD(strlen(buf));
	}
	if (!has_date) {
		(void)snprintf(buf, sizeof(buf), "Date: %s\n", t822);
		WOD(strlen(buf));
	}
	if (!has_from) {
		(void)snprintf(buf, sizeof(buf), "From: %s\n", ctx->from);
		WOD(strlen(buf));
	}
	if (!has_to) {
		(void)snprintf(buf, sizeof(buf), "To: %s\n", ctx->rcpts[0]);
		WOD(strlen(buf));
	}

	/*
	 * Add an empty line if original mail had no header/body distinction,
	 * that is, had no empty line itself.
	 * We believe the entire date given must be body then.
	 */
	if (!flag && write(fd, "\n", 1) != 1) {
		(void)close(fd);
		goto error_out;
	}

	/*
	 * Finally, write out the mail data.
	 */
	if (write(fd, ctx->data, ctx->size) != (ssize_t)ctx->size) {
		(void)close(fd);
		goto error_out;
	}

	/*
	 * Make sure the spool file is _really_ on disk.
	 */
	if (fsync(fd) != 0) {
		(void)close(fd);
		goto error_out;
	}
	if (close(fd) != 0)
		goto error_out;

	if (rename(temp, final) != 0) {
		xfatal("{%s} fatal: spool() => unable to \"mv\" file <%s> to <%s>", ctx->conn_id, temp, final);
		goto error_out;
	}

	nsm_free(t822);

	return 0;

error_out:
	xfatal("{%s} fatal: spool() => dropped out", ctx->conn_id);
	(void)unlink(temp);
	nsm_free(t822);

	return 2;
}

int
update_spool(const char *subdir, const char *dirfn, const SMTP_CTX *ctx)
{
	FILE *fp;
	char buf[1024];
	char fn[256];
	char *p;
	long int oldpos, curpos;
	int pflag;

	(void)snprintf(fn, sizeof(fn), "%s/%s/%s", nsmc_spool_root, subdir, dirfn);

	DEBUG(("update_spool() => spoolfile <%s>", fn));

	/*
	 * (Re)move spoolfile if no rcpt left
	 */
	pflag = 0;
	if (ctx->rcpts == NULL || ctx->rcpts[0] == NULL) {
		if (ctx->defers != NULL && ctx->defers[0] != NULL) {
			/*
			 * We have only defers left,
			 * so park this mail in defer-spool
			 * after we're done
			 */
			 pflag = 1;
		} else {
			/*
			 * Nothing left at all, remove
			 */
			if (unlink(fn) != 0) {
				xwarn("warn: update_spool() => unable to unlink spool file <%s>: \"%s\"", fn, strerror(errno));
				return 1;
			}
			xinfo("{%s}|%s| info: DONE, no more rcpts", ctx->conn_id, ctx->lmid);
			return 0;
		}
	}

	if ((fp = fopen(fn, "r+")) == NULL) {
		xwarn("warn: update_spool() => unable to open spool file <%s> r+: \"%s\"", fn, strerror(errno));
		return 1;
	}

	#define SWOD()		curpos = ftell(fp); \
				if (fseek(fp, oldpos, SEEK_SET) != 0) { \
					xwarn("warn: update_spool() => seek failed for <%s> pos (%ld): \"%s\"", fn, oldpos, strerror(errno)); \
					goto err_out; \
				} \
				if (fwrite(buf, strlen(buf), 1, fp) != 1) { \
					xwarn("warn: update_spool() => fwrite overwrite failed for <%s> pos (%ld): \"%s\"", fn, oldpos, strerror(ferror(fp))); \
					goto err_out; \
				} \
				if (fseek(fp, curpos, SEEK_SET) != 0) { \
					xwarn("warn: update_spool() => seek back for <%s> pos (%ld) failed: \"%s\"", fn, curpos, strerror(errno)); \
					goto err_out; \
				}

	while (oldpos = ftell(fp), fgets(buf, sizeof(buf), fp) != NULL) {
		if (*buf == '\n' || *buf == '-')
			goto out;
		switch(*buf) {
		/* From, Time, and Message ID cannot be changed after initially beeing spooled */
		case 'T': /* FALLTHRU */
		case 'M': /* FALLTHRU */
		case 'S': /* FALLTHRU */
		case '#':
			/* ignore */
			break;
		case 'R':
			buf[strlen(buf)-1] = '\0';
			if (!is_rcpt(ctx, buf+1)) {
				/*
				 * We need to remove or defer this recipient:
				 * we do this by changing the first char of the
				 * current line to '#' or 'D'
				 */
				if (is_deferred(ctx, buf+1)) {
					DEBUG(("update_spool() => making rcpt <%s> deferred rcpt", buf+1));
					*buf = 'D';
				} else {
					DEBUG(("update_spool() => removing recipient <%s>", buf+1));
					*buf = '#';
				}
				SWOD();
			}
			break;
		case 'D':
			buf[strlen(buf)-1] = '\0';
			if (!is_deferred(ctx, buf+1)) {
				/* see above */
				if (is_rcpt(ctx, buf+1)) {
					DEBUG(("update_spool() => making deferred rcpt <%s> normal rcpt again", buf+1));
					*buf = 'R';
				} else {
					DEBUG(("update_spool() => removing deferred recipient <%s>", buf+1));
					*buf = '#';
				}
				SWOD();
			}
			break;
		case 'F': /* FALLTHRU */
		case 'E':
			if (*buf == 'E' && ctx->exploded != 1) {
				DEBUG(("update_spool() => changing exploded E header to normal F header"));
				*buf = 'F';
				SWOD();
			} else if (*buf == 'F' && ctx->exploded == 1) {
				DEBUG(("update_spool() => changing F header to exploded E header"));
				*buf = 'E';
				SWOD();
			}
			break;
		default:
			xwarn("warn: update_spool() => spoofile <%s> has unknown header field '%c', ignoring", fn, *buf);
			break;
		}
	}

out:
	(void)fflush(fp);
	(void)fclose(fp);

	if (pflag) {
		/*
		 * PARK
		 */
		char cleanfn[256];
		int d_count;

		d_count = 0;
		(void)snprintf(cleanfn, sizeof(cleanfn), "%s", dirfn);

		if ((p = strstr(cleanfn, ";D=")) != NULL) {
			d_count = atol(p+3);
			*p = '\0';
		}

		if ((p = strchr(cleanfn, '!')) != NULL)
			*p = '\0';

		d_count++;

		if (d_count > nsmc_spool_max_defers && time(NULL) - ctx->spooltime > nsmc_spool_max_age) {
			xinfo("{%s}|%s| info: PURGE, has been in the queue too long, farewell", ctx->conn_id, ctx->lmid);
			if (ndn_tooold(ctx, (const char **)ctx->defers, time(NULL)) == 0) {
				(void)unlink(fn);
				return 0;
			}
			xwarn("{%s}|%s| warn: ndn_tooold() failed for some reason, this is bad.  We will fallback to PARK.");
		}

		(void)snprintf(buf, sizeof(buf), "%s/%s;D=%d;T=%lu", nsmc_spool_defer, cleanfn, d_count, time(NULL));
		if (rename(fn, buf) != 0) {
			 xwarn("warn: update_spool() => parking with rename() failed for file <%s>: \"%s\"", fn, strerror(errno));
			 return 1;
		}
		xinfo("{%s}|%s| info: PARK, only defers left, parking", ctx->conn_id, ctx->lmid);
	}

	return 0;

err_out:
	(void)fflush(fp);
	(void)fclose(fp);

	return 1;
}

void
do_dir(const char *subdir, void (*fn)(const char *, const char *))
{
	char fulldir[256], fullfile[256], lockfn[128];
	DIR *dp;
	struct dirent *entry;
	struct stat sb;

	assert(subdir != NULL);
	assert(*subdir != '\0');
	assert(fn != NULL);

	DEBUG(("do_dir() => spool subdir <%s>", subdir));

	(void)snprintf(fulldir, sizeof(fulldir), "%s/%s", nsmc_spool_root, subdir);

	if ((dp = opendir(fulldir)) == NULL) {
		xwarn("warn: do_dir() => unable to open spool dir <%s>", fulldir);
		return;
	}

	while ((entry = readdir(dp)) != NULL) {
		if (strncmp(entry->d_name, ".", 1) == 0)
			continue;

		DEBUG(("do_dir() => current entry <%s>", entry->d_name));

		(void)snprintf(fullfile, sizeof(fullfile), "%s/%s", fulldir, entry->d_name);
		if (stat(fullfile, &sb) != 0)
			continue;
		if ((sb.st_mode & S_IFMT) != S_IFREG) {
			xwarn("warn: do_dir() => <%s> is not a regular file", fullfile);
			continue;
		}

		/*
		 * Lock spool file
		 */
		if (try_lock_spool(subdir, entry->d_name, lockfn, sizeof(lockfn)) == 0) {
			fn("lock", lockfn);
			unlock_spool(subdir, lockfn, sizeof(lockfn));
		} else {
			DEBUG(("do_dir() => was unable to acquire lock for current entry <%s>, ignoring", entry->d_name));
		}
	}

	DEBUG(("do_dir() => done reading"));

	(void)closedir(dp);
}

int
lock_spool(const char *subdir, const char *dirfn, char *lockfn, size_t lockfn_len)
{
	char full_fn[256], full_lockfn[256], tmplockfn[128];

	(void)snprintf(full_fn, sizeof(full_fn), "%s/%s/%s", nsmc_spool_root, subdir, dirfn);
	(void)snprintf(tmplockfn, sizeof(tmplockfn), "%s!%u.%lu", dirfn, getpid(), random()%100000 + 1);
	(void)snprintf(full_lockfn, sizeof(full_lockfn), "%s/%s", nsmc_spool_lock, tmplockfn);

	if (rename(full_fn, full_lockfn) != 0) {
		DEBUG(("lock_spool() => failed. subdir <%s> file <%s>: \"%s\"", subdir, dirfn, strerror(errno)));
		return 1;
	}

	(void)snprintf(lockfn, lockfn_len, "%s", tmplockfn);

	DEBUG(("lock_spool() => success. subdir <%s> file <%s>", subdir, dirfn));

	return 0;
}

int
try_lock_spool(const char *subdir, const char *dirfn, char *lockfn, size_t lockfn_len)
{
	int i;

	for (i = 0; i < 5; i++) {
		if (lock_spool(subdir, dirfn, lockfn, lockfn_len) == 0)
			return 0;
		(void)usleep(100000);
		(void)usleep((useconds_t)(random()%1000 + 1) * 1000);
		if (i > 1)
			(void)sleep((unsigned int)i);
	}

	return 1;
}

void
unlock_spool(const char *subdir, char *lockfn, size_t lockfn_len)
{
	struct stat sb;
	char full_lockfn[256], full_cleanfn[256], cleanfn[128];
	char *p;

	(void)snprintf(full_lockfn, sizeof(full_lockfn), "%s/%s", nsmc_spool_lock, lockfn);

	(void)snprintf(cleanfn, sizeof(cleanfn), "%s", lockfn);
	if ((p = strchr(cleanfn, '!')) == NULL)
		xdie("fatal: unlock_spool() => delimiter '!' not found in filename, this should never happen");
	*p = '\0';

	(void)snprintf(full_cleanfn, sizeof(full_cleanfn), "%s/%s/%s", nsmc_spool_root, subdir, cleanfn);

	if (stat(full_lockfn, &sb) == 0 && rename(full_lockfn, full_cleanfn) != 0)
		xdie("fatal: unlock_spool() => rename(%s, %s) failed, this should never happen, error message is: \"%s\"", full_lockfn, full_cleanfn, strerror(errno));

	(void)snprintf(lockfn, lockfn_len, "%s", cleanfn);
}

int
is_in_list(const char **a, const char *n)
{
	int i;

	assert(n != NULL);
	assert(*n != '\0');

	if (a == NULL)
		return 0;

	for (i = 0; a[i] != NULL; i++)
		if (strcasecmp(a[i], n) == 0)
			return 1;

	return 0;
}

/*
 * return codes:
 * 0 - is not deferred rcpt
 * 1 - is deferred rcpt
 */
int
is_deferred(const SMTP_CTX *ctx, const char *rcpt)
{

	return is_in_list((const char **)ctx->defers, rcpt);
}

int
is_rcpt(const SMTP_CTX *ctx, const char *rcpt)
{

	return is_in_list((const char **)ctx->rcpts, rcpt);
}

void
defer_rcpt(const char *subdir, const char *dirfn, SMTP_CTX *ctx, const char *rcpt)
{
	int i;

	assert(rcpt != NULL);
	assert(ctx != NULL);
	assert(ctx->rcpts != NULL);

	/* unused */
	(void)subdir;
	(void)dirfn;

	DEBUG(("defer_rcpt() => subdir <%s> file <%s> rcpt <%s>", subdir, dirfn, rcpt));

	if (ctx->defers == NULL) {
		ctx->defers = nsm_calloc(1, sizeof(char *));
		ctx->defers[0] = NULL;
	}

	i = 0;
	while (ctx->defers[i] != NULL)
		i++;

	ctx->defers = nsm_realloc(ctx->defers, (i+2) * sizeof(char *));

	ctx->defers[i] = nsm_strdup(rcpt);
	ctx->defers[i+1] = NULL;
}
