/* This file is part of Mailfromd.
   Copyright (C) 2005-2025 Sergey Poznyakoff

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option)
   any later version.

   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, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#include <mailutils/mailutils.h>
#include <mailutils/cli.h>
#include <mailutils/daemon.h>

#include "libmf.h"
#include "srvcfg.h"
#include "callout.h"
#include "mfdb.h"
#include "callout-dbgmod.h"

char *ehlo_domain;
char *mailfrom_address;
int enable_vrfy;
char *session_id;

/* FIXME: Rewrite I/O via mu_stream */

struct callout_command {
	const char *command;
	int argmin;
	int argmax;
	int (*handler) (FILE *, int, char **);
};

enum callout_mode {
	cmode_mx_first,
	cmode_mx_only,
	cmode_host_only,
	cmode_host_first
};

struct vrfy_queue {
	struct vrfy_queue *next;
	unsigned serial;
	enum callout_mode mode;
	char *hostname;
	smtp_io_t io;
	mf_status result;
	FILE *file;
};

static struct vrfy_queue *head, *tail;
static unsigned serial;

int
trimcrlf(char *buf)
{
	size_t len = strlen(buf);
	if (len >= 1 && buf[len-1] == '\n') {
		buf[--len] = 0;
		if (len >= 1 && buf[len-1] == '\r')
			buf[--len] = 0;
		return 0;
	}
	return 1;
}

#define SPFX "S: "
#define SPFXSIZ (sizeof(SPFX)-1)
#define SBUFSIZ 512

static void
writeout(FILE *fp, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	if (mu_debug_level_p(MF_SOURCE_SAVSRV, MU_DEBUG_PROT)) {
		static char fmtbuf[SPFXSIZ + SBUFSIZ + 3 + 1 + 1] = SPFX;
		size_t fmtlen = strlen(fmt);
		char *dots = NULL;
		int more = fmt[fmtlen - 1] != '\n';
		if (fmtlen > SBUFSIZ) {
			fmtlen = SBUFSIZ;
			dots = "...";
		}
		if (fmt[fmtlen - 1] == '%' && fmt[fmtlen - 2] != '%')
			fmtlen--;
		memcpy(fmtbuf + SPFXSIZ, fmt, fmtlen);
		fmtbuf[SPFXSIZ + fmtlen] = 0;
		trimcrlf(fmtbuf);
		if (dots)
			strcat(fmtbuf, dots);
		if (more)
			strcat(fmtbuf, "\\");
		mu_diag_voutput(MU_LOG_DEBUG, fmtbuf, ap);
	}
	vfprintf(fp, fmt, ap);
	va_end(ap);
}

static void
defproctitle()
{
	if (session_id)
		mf_proctitle_format("callout server: %s", session_id);
	else
		mf_proctitle_format("callout server");
}

static void
vrfy_free(struct vrfy_queue *qp)
{
	smtp_io_free(qp->io);
	free(qp->hostname);
	free(qp);
}

static int
vrfy_del(unsigned num)
{
	struct vrfy_queue *qp, *prev;

	if (!head)
		return -1;
	if (head->serial == num) {
		qp = head;
		head = head->next;
		if (!head)
			tail = NULL;
		vrfy_free(qp);
		return 0;
	}

	for (prev = head, qp = head->next; qp; prev = qp, qp = qp->next) {
		if (qp->serial == num) {
			prev->next = qp->next;
			vrfy_free(qp);
			return 0;
		}
	}
	return -1;
}

static void
verify(struct vrfy_queue *qp, int hard)
{
	mf_status rc;
#define STATUS_FIXUP(code)						\
	do {								\
		if (hard && (code) == mf_timeout)			\
			(code) = mf_not_found;				\
        } while(0)

	smtp_io_set_timeouts(qp->io,
			     hard ? smtp_timeout_hard : smtp_timeout_soft);
	switch (qp->mode) {
	case cmode_mx_first:
		mf_proctitle_format("callout(mx): %s: %010u:<%s>",
				    smtp_io_id(qp->io),
				    qp->serial,
				    smtp_io_email(qp->io));
		if (!hard) {
			rc = cache_get(smtp_io_email(qp->io));
			if (rc != mf_failure)
				break;
		}
		rc = callout_standard(qp->io);
		STATUS_FIXUP(rc);
		cache_insert(smtp_io_email(qp->io), rc);
		break;

	case cmode_mx_only:
		mf_proctitle_format("callout(mx only): %s: %010u:<%s>@%s",
				    smtp_io_id(qp->io),
				    qp->serial,
				    smtp_io_email(qp->io),
				    qp->hostname);
		rc = callout_mx(qp->io, qp->hostname, NULL);
		STATUS_FIXUP(rc);
		break;
		
	case cmode_host_first:
		mf_proctitle_format("callout(host first): %s: %010u:<%s>@%s",
				    smtp_io_id(qp->io),
				    qp->serial,
				    smtp_io_email(qp->io),
				    qp->hostname);
		if (!hard) {
			rc = cache_get2(smtp_io_email(qp->io), qp->hostname);
			if (rc != mf_failure)
				break;
		}			
		rc = callout_strict(qp->io, qp->hostname);
		STATUS_FIXUP(rc);
		cache_insert2(smtp_io_email(qp->io),
			      qp->hostname, rc);
		break;

	case cmode_host_only:
		mf_proctitle_format("callout(host only): %s: %010u:<%s>@%s",
				    smtp_io_id(qp->io),
				    qp->serial,
				    smtp_io_email(qp->io),
				    qp->hostname);
		rc = callout_host(qp->io, qp->hostname);
		STATUS_FIXUP(rc);
	}
	qp->result = rc;
}

static void
run_queue(FILE *fp, int hard)
{
	struct vrfy_queue *qp;

	for (qp = head; qp; qp = qp->next) {
		qp->file = fp;
		verify(qp, hard);
	}
	defproctitle();
}

static void
savsrv_smtp_io_callback(void *data, const char *key, const char *value)
{
	struct vrfy_queue *qp = data;
	
	if (qp->file)
		writeout(qp->file, "* %010u %s %s\r\n",
			 qp->serial, key, value);
}

static char *
getval(char *input)
{
	char *p = strchr(input, '=');
	if (!p)
		return NULL;
	*p++ = 0;
	return p;
}

static struct mu_kwd tls_kwd[] = {
	{ "never",      tls_never },
	{ "always",	tls_always },
	{ "ondemand",   tls_ondemand },
	{ NULL }
};

int
smtp_str_to_starttls(char const *str, smtp_starttls_t *val)
{
	int v;
	if (mu_kwd_xlat_name(tls_kwd, str, &v))
		return -1;
	*val = v;
	return 0;
}

char const *
smtp_starttls_to_str(smtp_starttls_t t)
{
	char const *val = NULL;
	mu_kwd_xlat_tok(tls_kwd, t, &val);
	return val;
}

static struct vrfy_queue *
addq(int argc, char **argv, const char **errp)
{
	int i;
	struct vrfy_queue *qp = mu_alloc(sizeof(*qp));
	char *email;
	char *host = NULL;
	char *ehlo = ehlo_domain;
	char *mailfrom = mailfrom_address;
	enum callout_mode mode = cmode_mx_first;
	smtp_starttls_t tls = smtp_use_tls;

	static struct mu_kwd mode_kwd[] = {
		{ "mxfirst",    cmode_mx_first },
		{ "default",    cmode_mx_first },
		{ "mxonly",     cmode_mx_only },
		{ "hostonly",   cmode_host_only },
		{ "hostfirst",  cmode_host_first },
		{ NULL }
	};

	email = argv[1];
	for (i = 2; i < argc; i++) {
		char *attr = argv[i];
		char *val = getval(attr);
		
		if (!val) {
			*errp = "syntax error";
			return NULL;
		}
		if (!strcasecmp(attr, "host")) {
			host = val;
		} else if (!strcasecmp(attr, "mode")) {
			int v;
			if (mu_kwd_xlat_name(mode_kwd, val, &v)) {
				*errp = "unrecognized value for mode";
				return NULL;
			}
			mode = v;
		} else if (!strcasecmp(attr, "ehlo")) {
			ehlo = val;
		} else if (!strcasecmp(attr, "mailfrom")) {
			mailfrom = val;
		} else if (!strcasecmp(attr, "starttls")) {
			if (smtp_str_to_starttls(val, &tls)) {
				*errp = "unrecognized value for tls";
				return NULL;
			}
		} else {
			*errp = "unknown attribute";
			return NULL;
		}
	}
	
	switch (mode) {
	case cmode_host_first:
	case cmode_host_only:
	case cmode_mx_only:
		if (!host) {
			*errp = "mode requires host";
			return NULL;
		}
		break;
	case cmode_mx_first:
		break;
	}

	qp->serial = serial++;
	qp->result = mf_temp_failure; /* FIXME */
	qp->io = smtp_io_create(session_id ? session_id : email,
				smtp_timeout_hard,
				savsrv_smtp_io_callback,
				qp);
	smtp_io_setup_callout(qp->io, email, ehlo, mailfrom, tls);
	switch (mode) {
	case cmode_host_first:
	case cmode_host_only:
	case cmode_mx_only:
		qp->hostname = mu_strdup(host);
		break;
	case cmode_mx_first:
		qp->hostname = NULL;
		break;
	}
	qp->mode = mode;

	qp->next = NULL;
	if (tail)
		tail->next = qp;
	else
		head = qp;
	tail = qp;
	return qp;
}

int
cmd_vrfy(FILE *fp, int argc, char **argv)
{
	const char *errp = "syntax error";
	struct vrfy_queue *qp = addq(argc, argv, &errp);
	if (qp)
		writeout(fp, "OK %010u\r\n", qp->serial);
	else
		writeout(fp, "NO %s\r\n", errp);
	return 0;
}

int
cmd_get(FILE *fp, int argc, char **argv)
{
	int i;

	for (i = 1; i < argc; i++) {
		char *attr = argv[i];
		char const *val;
		if (!mu_c_strcasecmp(attr, "ehlo"))
			val = ehlo_domain;
		else if (!mu_c_strcasecmp(attr, "mailfrom"))
			val = mailfrom_address;
		else if (!mu_c_strcasecmp(attr, "transcript"))
			val = smtp_transcript ? "ON" : "OFF";
		else if (!mu_c_strcasecmp(attr, "starttls")) {
			val = smtp_starttls_to_str(smtp_use_tls);
		} else /* FIXME: Timeouts */
			val = NULL;
		if (val)
			writeout(fp, "* %s=%s\r\n", attr, val);
	}
	writeout(fp, "OK\r\n");
	return 0;
}

int
cmd_set(FILE *fp, int argc, char **argv)
{
	int i;
	smtp_starttls_t tls;

	for (i = 1; i < argc; i++) {
		char *val = strchr(argv[i], '=');
		if (!val) {
			writeout(fp, "NO bad assignment %s\n", argv[i]);
			return 1;
		}
		if (!mu_c_strncasecmp(argv[i], "ehlo", 4) ||
		    !mu_c_strncasecmp(argv[i], "mailfrom", 8)) {
			/* OK */;
		} else if (!mu_c_strncasecmp(argv[i], "transcript", 10)) {
			val++;
			if (mu_c_strcasecmp(val, "ON") &&
			    mu_c_strcasecmp(val, "OFF")) {
				writeout(fp, "NO bad assignment %s\n", argv[i]);
				return 1;
			}
		} else if (!mu_c_strncasecmp(argv[i], "starttls", 3)) {
			val++;
			if (smtp_str_to_starttls(val, &tls)) {
				writeout(fp, "NO unrecognized starttls mode: %s\n", val);
				return 1;
			}
		} else {
			writeout(fp, "NO bad assignment %s\n", argv[i]);
			return 1;
		}
	}
			
	for (i = 1; i < argc; i++) {
		char *attr = argv[i];
		char *val = strchr(attr, '=') + 1;

		if (!mu_c_strncasecmp(attr, "ehlo", 4)) {
			free(ehlo_domain);
			ehlo_domain = mu_strdup(val);
		} else if (!mu_c_strncasecmp(attr, "mailfrom", 8)) {
			free(mailfrom_address);
			mailfrom_address = mu_strdup(val);
		} else if (!mu_c_strncasecmp(attr, "transcript", 10)) {
			smtp_transcript = mu_c_strcasecmp(val, "ON") == 0;
		} else if (!mu_c_strncasecmp(attr, "starttls", 3)) {
			smtp_use_tls = tls;
		}
	}
	writeout(fp, "OK\r\n");
	return 0;
}

int
cmd_sid(FILE *fp, int argc, char **argv)
{
	if (session_id)
		free(session_id);
	session_id = mu_strdup(argv[1]);
	defproctitle();
	writeout(fp, "OK\r\n");
	return 0;
}

int
cmd_timeout(FILE *fp, int argc, char **argv)
{
	time_t to[SMTP_NUM_TIMEOUT];
	int i;
		
	for (i = 1; i < argc; i++) {
		char *p;
		unsigned long n = strtoul(argv[i], &p, 10);
		if (*p) {
			writeout(fp, "NO syntax error in #%d\r\n", i);
			return 0;
		}
		to[i-1] = (time_t) n;
	}
	memcpy(smtp_timeout_soft, to, sizeof(to));
	writeout(fp, "OK timeouts set\r\n");
	return 0;
}

int
cmd_run(FILE *fp, int argc, char **argv)
{
	struct vrfy_queue *qp, *prev;
	run_queue(fp, 0);
	writeout(fp, "OK");
	
	for (qp = head, prev = NULL; qp; ) {
		struct vrfy_queue *next = qp->next;
		mf_status result = (qp->result == mf_timeout) ?
			             mf_temp_failure : qp->result;
		writeout(fp, " %010u=%s", qp->serial,
			 mf_status_str(result));
		if (qp->result != mf_failure && qp->result != mf_timeout) {
			if (qp == tail)
				tail = prev;
			vrfy_free(qp);
			if (prev)
				prev->next = next;
			else
				head = next;
		} else
			prev = qp;
		qp = next;
	}
	writeout(fp, "\r\n");
	return 0;
}

int
cmd_drop(FILE *fp, int argc, char **argv)
{
	if (strcasecmp(argv[1], "ALL") == 0) {
		struct vrfy_queue *qp;

		for (qp = head; qp; ) {
			struct vrfy_queue *next = qp->next;
			vrfy_free(qp);
			qp = next;
		}
		head = tail = NULL;
	} else {
		char *p;
		unsigned num = strtoul(argv[1], &p, 10);
		if (*p) 
			writeout(fp, "NO syntax error\r\n");
		else if (vrfy_del(num))
			writeout(fp, "NO entry not found\r\n");
		else
			writeout(fp, "OK\r\n");
	}
	return 0;
}

int
cmd_quit(FILE *fp, int argc, char **argv)
{
	writeout(fp, "OK bye\r\n");
	return 1;
}

static struct callout_command callout_command_tab[] = {
	{ "VRFY", 2, 0, cmd_vrfy },
	{ "GET", 2, 0, cmd_get },
	{ "SET", 2, 0, cmd_set },
	{ "SID", 2, 2, cmd_sid },
	{ "TIMEOUT", SMTP_NUM_TIMEOUT+1, SMTP_NUM_TIMEOUT+1, cmd_timeout },
	{ "RUN", 1, 1, cmd_run },
	{ "QUIT", 1, 1, cmd_quit },
	{ "DROP", 2, 2, cmd_drop },
	{ NULL }
};

static struct callout_command *
find_command(const char *input)
{
	struct callout_command *cmd;

	for (cmd = callout_command_tab; cmd->command; cmd++) {
		if (strcasecmp(cmd->command, input) == 0)
			return cmd;
	}
	return NULL;
}


int
callout_session_server(const char *id, int fd,
		       struct sockaddr const *sa, socklen_t len,
		       void *server_data, void *srvman_data)
{
	FILE *fp = fdopen(fd, "w+");
	char buf[1024];
	int longline = 0;
	
	signal(SIGPIPE, SIG_IGN);
	signal(SIGALRM, SIG_IGN);

	defproctitle();

	setvbuf(fp, NULL, _IOLBF, 0);
	writeout(fp, "OK mailfromd callout server ready\r\n");
	while (fgets(buf, sizeof(buf), fp)) {
		struct mu_wordsplit ws;
		struct callout_command *cmd;
		int rc = 0;
		
		if (trimcrlf(buf)) {
			mu_debug(MF_SOURCE_SAVSRV, MU_DEBUG_PROT,
				 ("%c: %s", longline ? '>' : 'C', buf));
			longline = 1;
			continue;
		}

		if (longline) {
			writeout(fp, "NO bad input\r\n");
			longline = 0;
			continue;
		}
		
		mu_debug(MF_SOURCE_SAVSRV, MU_DEBUG_PROT, ("C: %s", buf));

		if (mu_wordsplit(buf, &ws, MU_WRDSF_DEFFLAGS)) {
			writeout(fp, "NO cannot parse line\r\n");
			continue;
		}

		if (ws.ws_wordc == 0)
			writeout(fp, "NO empty command\r\n");
		else {
			cmd = find_command(ws.ws_wordv[0]);
			if (!cmd)
				writeout(fp, "NO unknown command\r\n");
			else if ((cmd->argmin && ws.ws_wordc < cmd->argmin)
				 || (cmd->argmax && ws.ws_wordc > cmd->argmax))
				writeout(fp, "NO invalid arguments\r\n");
			else
				rc = cmd->handler(fp, ws.ws_wordc,
						  ws.ws_wordv);
		}
		mu_wordsplit_free(&ws);
		if (rc)
			break;
	}
	fclose(fp);
	
	/* Run queued verifications */
	run_queue(NULL, 1);
	return 0;
}

