/*
 * $Id: 17e97691da101219c387e0cf9c4ee70e20f8275e $
 *
 * 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 <syslog.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <time.h>
#include <errno.h>
#include <signal.h>

#include "ldap.h"

#include "ntl.h"
#include "ntlc.h"
#include "alloc.h"
#include "dns.h"
#include "spool.h"
#include "log.h"
#include "misc.h"
#include "ntu.h"
#include "check.h"
#include "spamscan.h"
#include "ndn.h"
#include "smtp.h"
#include "proctitle.h"
#include "config.h"

static void do_file(const char *, const char *);
static int try_deliver(SMTP_CTX *, const char *, const char *, const char *);
static void setinfo(SMTP_CTX *, const char *, ...);
static void sigterm(int);

static const char rcsid[] = "$Id: 17e97691da101219c387e0cf9c4ee70e20f8275e $";

static const char str_local[] = "localhost";
static const char str_127[] = "127.0.0.1";

static unsigned long long cycles;
static int want_down = 0;

static LDAP *ld;

extern int ntl_error;

/*
 * return codes:
 * <0 - temporal error
 * 0  - success
 * >0 - apparently not responsible
 * 2  - responsible but not found in LDAP
 */
static int
try_deliver(SMTP_CTX *ctx, const char *rcpt, const char *subdir, const char *fn)
{
	LDAPMessage *res, *entry;
	NTL_ATTR *attr;
	char buf[1024], buf2[1024];
	const char **forwards;
	const char *maildir, *domain, *mx;
	int e;
	uid_t uid;
	gid_t gid;

	domain = strchr(rcpt, '@') + 1;

	DEBUG(("try_deliver() => ctx: lmid <%s>, rcpt <%s> domain <%s>", ctx->lmid, rcpt, domain));

	/*
	 * (1) Check if rcpt is indeed local
	 */
	(void)snprintf(buf, sizeof(buf), "cn=%s,ou=config,o=mail,dc=noxa,dc=de", nsmc_name);
	(void)snprintf(buf2, sizeof(buf2), "rcpthost=%s", domain);
	if ((res = ntl_search(ld, buf, LDAP_SCOPE_BASE, buf2, NULL)) == NULL
	    || (entry = ldap_first_entry(ld, res)) == NULL) {
		DEBUG(("try_deliver() => not local, not even responsible for <%s>", buf2));
		if (res != NULL)
			(void)ldap_msgfree(res);
		return 1;
	}
	(void)ldap_msgfree(res);

	(void)snprintf(buf, sizeof(buf), "ou=%s,o=mail,dc=noxa,dc=de", domain);
	if ((res = ntl_search(ld, buf, LDAP_SCOPE_BASE, NULL, NULL)) == NULL
	    || (entry = ldap_first_entry(ld, res)) == NULL) {
		DEBUG(("try_deliver() => nope, domain <%s> is not local", domain));
		if (res != NULL)
			(void)ldap_msgfree(res);
		return 1;
	}
	(void)ldap_msgfree(res);

	/*
	 * (1)(b) Really best preference MX?
	 *
	 * XXX/NOTE:  We don't handle multiple MX with same preference sillyness here.
	 *            (for example: 10 a.mx.domain.de, 10 b.mx.domain.de)
	 *            We would just check the first atm, which could be either a or b.
	 */
	(void)get_next_mx(NULL);
	if ((mx = get_next_mx(rcpt)) == NULL) {
		DEBUG(("try_deliver() => no MX for domain <%s> found", domain));
		return 1;
	}

	if (!is_mx_us(mx, 25)) {
		DEBUG(("try_deliver() => not best preference MX for domain <%s>", domain));
		return 1;
	}

	/*
	 * We're responsible
	 */
	setinfo(ctx, "try <%s>", rcpt);

	/*
	 * (2) Get spool settings:  dir, forwards?, et c.
	 */
	(void)snprintf(buf, sizeof(buf), "(|(mail=%s)(mailAlternateAddress=%s))", rcpt, rcpt);
	if ((res = ntl_search(ld, "o=mail,dc=noxa,dc=de", LDAP_SCOPE_SUBTREE, buf, NULL)) == NULL
	    || (entry = ldap_first_entry(ld, res)) == NULL) {
		setinfo(ctx, "no such mailbox <%s>", rcpt);
		if (res != NULL)
			(void)ldap_msgfree(res);
		return 2;
	}
	attr = ntl_entry(ld, entry);
	(void)ldap_msgfree(res);

	/*
	 * (3) Deliver!
	 */

	/*
	 * (a) create spool file for forwards
	 */
	if (ctx->exploded != 1 && *(forwards = ntl_lookup(attr, "mailForwardingAddress")) != NULL) {
		SMTP_CTX *forward_ctx;
		int i, k;
		int prefix;

		setinfo(ctx, "forwards <%s> => expand", rcpt);
		forward_ctx = init_smtp_ctx();
		if (ctx->from != NULL)
			forward_ctx->from = nsm_strdup(ctx->from);
		if (ctx->conn_id != NULL)
			forward_ctx->conn_id = nsm_strdup(ctx->conn_id);
		forward_ctx->esmtp = ctx->esmtp;
		forward_ctx->lmid = make_mid(ctx);

		forward_ctx->helo = nsm_strdup(nsmc_name);
		forward_ctx->server = nsmc_name;
		forward_ctx->client_rn = str_local;
		forward_ctx->client_ip = str_127;

		nsm_asprintf(&(forward_ctx->recvdinfo), "(nsmtp/smtpl %d expanded recipients from forward)", getpid());

		forward_ctx->rcpts = nsm_calloc(1, sizeof(char *));
		for (k = 0, i = 0; forwards[i] != NULL; i++)
			switch (check_mailaddress(forwards[i], 2)) {
			case -1:
			case 4:
			case 0:
				forward_ctx->rcpts = nsm_realloc(forward_ctx->rcpts, (k + 2) * sizeof(char *));
				forward_ctx->rcpts[k] = nsm_strdup(forwards[i]);
				k++;
				break;
			case 1:
				xinfo("{%s}|%s| info: forward <%s> for <%s> is not a valid mail address", ctx->conn_id, ctx->lmid, forwards[i], rcpt);
				break;
			case 2:
				xwarn("{%s}|%s| info: forward <%s> for <%s> is local but does not exist", ctx->conn_id, ctx->lmid, forwards[i], rcpt);
				break;
			default:
				xwarn("{%s}|%s| warn: internal error: unhandled return value of check_mailaddress() for forwards", ctx->conn_id, ctx->lmid);
				break;
			}
		forward_ctx->rcpts[k] = NULL;

		if (i > 0) {
			forward_ctx->data = nsm_calloc(ctx->size + 256, sizeof(char *));
			prefix = snprintf(forward_ctx->data, 256, "Delivered-To: %s\n", rcpt);
			if (prefix >= 256)
				prefix = 255;
			(void)memcpy(forward_ctx->data + prefix, ctx->data, ctx->size);
			forward_ctx->size = prefix + ctx->size;

			setinfo(ctx, "forwards <%s> => spool", rcpt);

			if (spool("incoming", forward_ctx) != 0) {
				xwarn("{%s}|%s| warn: creating forward spool file failed, exploding forwards another time", ctx->conn_id, ctx->lmid);
				destroy_smtp_ctx(forward_ctx);
				e = -1;
				goto error_out;
			} else {
				for (i = 0; forward_ctx->rcpts[i] != NULL; i++)
					xinfo("{%s}|%s| info: FORWARD for rcpt <%s> to <%s> #(%d), forward LMID=<%s>", ctx->conn_id, ctx->lmid, rcpt, forward_ctx->rcpts[i], i, forward_ctx->lmid);
				ctx->exploded = 1;
				if (update_spool(subdir, fn, ctx) != 0) {
					xfatal("{%s}|%s| fatal: try_deliver() => the unthinkable happend: update_spool() failed, we bail out", ctx->conn_id, ctx->lmid);
					destroy_smtp_ctx(forward_ctx);
					e = -1;
					goto error_out;
				}
			}
		}

		destroy_smtp_ctx(forward_ctx);
	}

	/*
	 * (b) check if:
	 *      (i)  we need to deliver to maildir:
	 *           that is, noLocalDelivery is FALSE or not set
	 *      (ii) and consequently if maildir exists
	 */
	if ((maildir = *ntl_lookup(attr, "mailMessageStore")) != NULL) {
		FILE *fp;
		char stagingfn[1024], targetfn[1024];
		const char **tmp;
		const char *extra;
		struct stat sb;

		if (ntl_lookup_val(attr, "noLocalDelivery", "TRUE")) {
			e = 0;
			goto error_out; /* really: okay */
		}

		if (lstat(maildir, &sb) != 0) {
			xwarn("{%s}|%s| warn: try_deliver() => lstat(\"%s\", ..) returned != 0: \"%s\"", ctx->conn_id, ctx->lmid, maildir, strerror(errno));
			e = -1;
			goto error_out;
		}

		if (!(sb.st_mode & S_IFDIR)) {
			xwarn("{%s}|%s| warn: try_deliver() => <%s> is not a dir", ctx->conn_id, ctx->lmid, maildir);
			e = -1;
			goto error_out;
		}

		(void)snprintf(buf, sizeof(buf), "%s/Maildir/new", maildir);
		if (lstat(buf, &sb) != 0) {
			xwarn("{%s}|%s| warn: try_deliver() => lstat(\"%s\", ..) returned != 0: \"%s\"", ctx->conn_id, ctx->lmid, buf, strerror(errno));
			e = -1;
			goto error_out;
		}
		(void)snprintf(buf, sizeof(buf), "%s/Maildir/tmp", maildir);
		if (lstat(buf, &sb) != 0) {
			xwarn("{%s}|%s| warn: try_deliver() => lstat(\"%s\", ..) returned != 0: \"%s\"", ctx->conn_id, ctx->lmid, buf, strerror(errno));
			e = -1;
			goto error_out;
		}

		/*
		 * (c) deliver to maildir
		 */

		extra = "";

		uid = nsmc_default_maildir_uid;
		gid = nsmc_default_maildir_gid;
		if (*(tmp = ntl_lookup(attr, "uidNumber")) != NULL) {
			uid = atol(*tmp);
			gid = (gid_t)uid;
		}

		/*
		 * (c)(1) spamscan, but only if not already done at DATA
		 */
		if (!ntl_lookup_val(attr, "onlineSpamscan", "TRUE")) {
			NTL_ATTR *d_attr;
			char *newdata;
			size_t newdatasize;
			const char *spamuser;
			int sr;

			setinfo(ctx, "spamscan <%s>", rcpt);

			/*
			 * Fallback to Domain Config if no spamassassin user config
			 */
			spamuser = rcpt;
			if (*ntl_lookup(attr, "spamassassin") == NULL)
				spamuser = domain;

			if ((sr = spamscan(spamuser, ctx->data, ctx->size, &newdata, &newdatasize)) >= 0) {
				if (ctx->mmap != NULL && ctx->mmap != MAP_FAILED) {
					(void)munmap(ctx->mmap, MAX_SIZE);
					ctx->mmap = NULL;
				} else
					nsm_free(ctx->data);
				ctx->data = newdata;
				ctx->size = newdatasize;

				/*
				 * Check if user wants spam delivered to a spam mailbox.
				 * If so, check if Maildir exists and set up if necessary.
				 */
				#define CIFNE()	if (stat(buf, &sb) != 0 && (mkdir(buf, 0750) != 0 || chown(buf, uid, gid) != 0)) { \
							xwarn("{%s}|%s| warn: try_deliver() => mkdir/chown(%s) failed: \"%s\"", ctx->conn_id, ctx->lmid, buf, strerror(errno)); \
							e = -1; \
							ntl_entry_free(d_attr); \
							goto error_out; \
						}
				if (sr > 0) {
					d_attr = NULL;
					if (want_spammailbox(rcpt, &d_attr, &attr, 1) > 0) {
						extra = nsmc_spam_mailbox;
						(void)snprintf(buf, sizeof(buf), "%s/Maildir%s", maildir, extra);
						CIFNE();
						(void)snprintf(buf, sizeof(buf), "%s/Maildir%s/new", maildir, extra);
						CIFNE();
						(void)snprintf(buf, sizeof(buf), "%s/Maildir%s/cur", maildir, extra);
						CIFNE();
						(void)snprintf(buf, sizeof(buf), "%s/Maildir%s/tmp", maildir, extra);
						CIFNE();
						xinfo("{%s}|%s| info: SPAM mail: will deliver to spam mailbox as requested by config", ctx->conn_id, ctx->lmid);
					}
					ntl_entry_free(d_attr);
				}
			}
		}

		setinfo(ctx, "deliver <%s>", rcpt);

		/*
		 * (c)(2) staging
		 */
		(void)snprintf(buf, sizeof(buf), "%lu.%lu.%s", (unsigned long)time(NULL), random() % 100000 + 1, ctx->lmid);
		(void)snprintf(stagingfn, sizeof(stagingfn), "%s/Maildir%s/tmp/%s", maildir, extra, buf);

		if ((fp = fopen(stagingfn, "a")) == NULL) {
			xwarn("{%s}|%s| warn: try_deliver() => fopen(\"%s\", \"a\") for staging file failed: \"%s\"", ctx->conn_id, ctx->lmid, stagingfn, strerror(errno));
			e = -1;
			goto error_out;
		}

		/*
		 * RfC2821 part 4.4, we are at final delivery, so we add Return Path above
		 */
		(void)snprintf(buf2, sizeof(buf2), "Return-Path: %s\nDelivered-To: %s\n", *ctx->from == '\0' ? "<>" : ctx->from, rcpt);
		if (fwrite(buf2, strlen(buf2), 1, fp) != 1) {
			xwarn("{%s}|%s| warn: try_deliver() => fwrite(\"Return-Path..Delivered-To..\", %lu) failed for staging file <%s>: \"%s\"", ctx->conn_id, ctx->lmid, strlen(buf2), stagingfn, strerror(ferror(fp)));
			(void)fclose(fp);
			(void)unlink(stagingfn);
			e = -1;
			goto error_out;
		}

		/*
		 * Target filename with Courier IMAP ",S=size" suffix
		 */
		(void)snprintf(targetfn, sizeof(targetfn), "%s/Maildir%s/new/%s,S=%lu", maildir, extra, buf, (long unsigned int)strlen(buf2) + ctx->size);


		/*
		 * (c)(3) write mail data
		 */
		if (fwrite(ctx->data, ctx->size, 1, fp) != 1) {
			xwarn("{%s}|%s| warn: try_deliver() => fwrite(data, %lu) failed for staging file <%s>: \"%s\"", ctx->conn_id, ctx->lmid, ctx->size, stagingfn, strerror(ferror(fp)));
			(void)fclose(fp);
			(void)unlink(stagingfn);
			e = -1;
			goto error_out;
		}

		(void)fflush(fp);
		if (fclose(fp) != 0) {
			xwarn("{%s}|%s| warn: try_deliver() => fclose() for staging file <%s> failed: \"%s\"", ctx->conn_id, ctx->lmid, stagingfn, strerror(errno));
			(void)unlink(stagingfn);
			e = -1;
			goto error_out;
		}

		/*
		 * (c)(4) changing permissions
		 */
		if (chown(stagingfn, uid, gid) != 0) {
			xwarn("{%s}|%s| warn: try_deliver() => chown(%d, %d) for staging file <%s> failed: \"%s\"", ctx->conn_id, ctx->lmid, uid, gid, stagingfn, strerror(errno));
			(void)unlink(stagingfn);
			e = -1;
			goto error_out;
		}
		if (chmod(stagingfn, 0640) != 0) {
			xwarn("{%s}|%s| warn: try_deliver() => chmod(0640) for staging file <%s> failed: \"%s\"", ctx->conn_id, ctx->lmid, stagingfn, strerror(errno));
			(void)unlink(stagingfn);
			e = -1;
			goto error_out;
		}

		/*
		 * (c)(5) committing
		 */
		if (rename(stagingfn, targetfn) != 0) {
			xwarn("{%s}|%s| warn: try_deliver() => rename() to commit from staging to target file <%s> failed: \"%s\"", ctx->conn_id, ctx->lmid, targetfn, strerror(errno));
			(void)unlink(stagingfn);
			e = -1;
			goto error_out;
		}

	} else {
		/*
		 * (d)
		 * Attribute mailMessageStore does not exist:
		 * Is "noLocalDelivery" set to TRUE, then?
		 * If not, defer.
		 */
		if (!ntl_lookup_val(attr, "noLocalDelivery", "TRUE")) {
			xwarn("{%s}|%s| warn: try_deliver() => unable to deliver locally because there is no mailMessageStore attribute", ctx->conn_id, ctx->lmid);
			e = -1;
			goto error_out;
		}
	}

	setinfo(ctx, "success <%s>", rcpt);
	e = 0;

error_out:
	if (e != 0)
		setinfo(ctx, "failure <%s>", rcpt);

	ntl_entry_free(attr);

	return e;
}

static void
do_file(const char *subdir, const char *fn)
{
	SMTP_CTX *ctx;
	long long oac;
	int i, e;

	DEBUG(("do_file() => spool subdir <%s>, file <%s>", subdir, fn));

	oac = nsm_alloc_reset(0);

	if ((ctx = read_spoolfile(subdir, fn)) == NULL) {
		xwarn("warn: do_file() => unable to read file: spool subdir <%s>, file <%s>", subdir, fn);
		goto dropout;
	}

	DEBUG(("do_file() => ctx: lmid: <%s>, from: <%s>, size (%lu)", ctx->lmid, ctx->from, (unsigned long)ctx->size));

	if (ctx->rcpts == NULL) {
		xwarn("warn: do_file() => spool file <%s> has no rcpts", fn);
		goto dropout;
	}

	for (i = 0; ctx->rcpts[i] != NULL; i++) {
		DEBUG(("do_file() => for-each-recipient, current <%s>", ctx->rcpts[i]));

		if ((e = try_deliver(ctx, ctx->rcpts[i], subdir, fn)) == 0) {
			xinfo("{%s}|%s| info: SUCCESS for rcpt <%s>", ctx->conn_id, ctx->lmid, ctx->rcpts[i]);
			*(ctx->rcpts[i]) = '\0';
		} else if (e == 2) {
			const char *rcptlist[2];
			xinfo("{%s}|%s| info: NOTFOUND for rcpt <%s>, user does not exist", ctx->conn_id, ctx->lmid, ctx->rcpts[i]);
			rcptlist[0] = ctx->rcpts[i];
			rcptlist[1] = NULL;
			ndn_bounce(ctx, rcptlist, NULL, "Mailbox does not exist", NULL, time(NULL));
			*(ctx->rcpts[i]) = '\0';
		} else if (e < 0) {
			xinfo("{%s}|%s| info: DEFERRAL for rcpt <%s>", ctx->conn_id, ctx->lmid, ctx->rcpts[i]);
			defer_rcpt(subdir, fn, ctx, ctx->rcpts[i]);
			*(ctx->rcpts[i]) = '\0';
		}

		(void)get_next_mx(NULL);
	}
	remove_empty_rcpts(ctx->rcpts);

	if (update_spool(subdir, fn, ctx) != 0)
		xfatal("{%s}|%s| fatal: do_file() => update_spool() failed after creating facts, this is bad", ctx->conn_id, ctx->lmid);

dropout:
	if (ctx != NULL)
		destroy_smtp_ctx(ctx);

	if (!nsm_alloc_check())
		xwarn("warn: do_file() => DEBUG/QA: nsm_alloc_check() reports failure");
	(void)nsm_alloc_reset(oac);
}

static void
setinfo(SMTP_CTX *ctx, const char *fmt, ...)
{
	char state[512];
	va_list ap;

	va_start(ap, fmt);
	(void)vsnprintf(state, sizeof(state), fmt, ap);
	va_end(ap);

	DEBUG(("setinfo() => state change <%s>", state));

	if (ctx == NULL) {
		setproctitle("@%lu %s", time(NULL), state);
		return;
	}

	setproctitle("@%lu |%s|<%s>%s%s", time(NULL), ctx->lmid, ctx->from, state != NULL ? " " : "", state != NULL ? state : "");
}

static void
sigterm(int ignore)
{

	(void)ignore;
	want_down = 1;
}

int
main(int argc, char *argv[])
{
	char subdir[128], progname[128];
	const char *cfgfile;
	int ch;

	setprogname(argv[0]);
	cfgfile = NSMTP_DEFAULT_CONFIG;

	while ((ch = getopt(argc, argv, "c:hv")) != -1)
		switch (ch) {
		case 'c':
			cfgfile = optarg;
			break;
		case 'v':
			(void)fprintf(stderr, "nsmtp Version %s\n", NSMTP_VERSION);
			(void)fprintf(stderr, "Copyright (c) 2007, 2008 Niklas Olmes <niklas@noxa.de>\n"
				      "                         http://www.nevm.de/nsmtp/\n"
				      "                         NEVM-RIPE\n"
				      "Licensed under the GNU GPL Version 2.\n");
			exit(EXIT_SUCCESS);
		case '?': /* FALLTHRU */
		case 'h': /* FALLTHRU */
		default:
			(void)fprintf(stderr, "Usage: %s [-c cfgfile] [-hv] spoolsubdir\n", getprogname());
			exit(EXIT_FAILURE);
		}

	if (argc - optind != 1) {
		(void)fprintf(stderr, "Usage error:  Missing parameter spoolsubdir on command line.\n");
		(void)fprintf(stderr, "%s -h for Usage.\n", getprogname());
		exit(EXIT_FAILURE);
	}

	(void)snprintf(subdir, sizeof(subdir), "%s", argv[optind]);
	(void)snprintf(progname, sizeof(progname), "%s %s", argv[0], subdir);
	setprogname(progname);

	openlog("nsmtp(smtpl)", LOG_PID, LOG_MAIL);
	if (nsm_config_read(cfgfile) != 0)
		nsm_config_perror("smtpl");
	read_ldap_cfg();
	xinfo("info: nsmtp smtpl started for host <%s>[%s] alias <%s> domain <%s>, initializing. spoolsubdir <%s>", nsmc_name, nsmc_ip, nsmc_alias, nsmc_domain, subdir);

	(void)signal(SIGTERM, sigterm);
	(void)signal(SIGPIPE, SIG_IGN);

	ntu_seed();

	if (chdir(nsmc_mail_root) != 0)
		xdie("fatal: chdir(%s) failed", nsmc_mail_root);

	ldap_connect(&ld);

	daemonize();

	initproctitle(argc, argv);
	setproctitle("started");

	cycles = 0;
	while (!want_down) {
		if (++cycles % 10000 == 0)
			xinfo("info: still alive after (%llu) cycles", cycles);

		if (ntl_error == LDAP_SERVER_DOWN) {
			setinfo(NULL, "ldap reconnect");
			if (ldap_reconnect(&ld) != 0) {
				setinfo(NULL, "ldap unavailable, waiting");
				(void)sleep((unsigned int)((random() % 60) + 30));
				continue;
			}
		}

		setproctitle("run");
		do_dir(subdir, do_file);

		setproctitle("idle");
		(void)sleep((unsigned int)((random() % 5) + 1));
	}

	xinfo("info: SIGTERM received, shutting down");
	setproctitle("shutdown");

	if (ld != NULL) {
		if (ntl_error != LDAP_SERVER_DOWN)
			(void)ldap_unbind(ld);
		else
			(void)ldap_ld_free(ld, 0, NULL, NULL);
	}

	nsm_config_free();
	free_ldap_cfg();

	if (!nsm_alloc_check())
		xwarn("warn: main() => DEBUG/QA: nsm_alloc_check() reports failure");

	xinfo("info: shutdown complete");

	exit(EXIT_SUCCESS);
}
