/* 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 <stdio.h>
#include <stdarg.h>
#include <syslog.h>
#include <signal.h>
#include <pwd.h>
#include <grp.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

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

#include "mailfromd.h"
#include "prog.h"
#include "srvman.h"
#include "srvcfg.h"


static const char *ctx_getsym(void *data, const char *str);
static int ctx_setreply(void *data, char *code, char *xcode, char *message);

/* Per-message data */

struct message_data {
	eval_environ_t env;        /* Evaluation environment */
	char *helostr;             /* Domain name obtained in HELO phase */
	char msgid[64];            /* Message ID */
};
static struct message_data *test_message_data;

void
test_message_data_init(eval_environ_t env)
{
	test_message_data = mu_alloc(sizeof(*test_message_data));
	test_message_data->env = env;
	test_message_data->helostr = NULL;
	test_message_data->msgid[0] = 0;
}

static struct message_data *
priv_get(SMFICTX *ctx)
{
	struct message_data *md;

	if (mode == MAILFROMD_TEST)
		md = test_message_data;
	else
		md = (struct message_data*) gacopyz_getpriv(ctx);

	if (!md) {
		md = malloc(sizeof(*md));
		if (!md)
			mu_error(_("not enough memory"));
		else {
			milter_sockaddr_t addr;
			socklen_t len = sizeof(addr);

			md->env = create_environment(ctx,
						     ctx_getsym,
						     ctx_setreply,
						     NULL,
						     ctx);
			clear_rcpt_count(md->env);
			md->helostr = NULL;
			md->msgid[0] = 0;
			gacopyz_setpriv(ctx, md);
			env_init(md->env);
			if (gacopyz_server_sockname(ctx, &addr, &len) == 0)
				set_milter_server_address(md->env, &addr, len);
			set_milter_server_id(md->env, gacopyz_getclosure(ctx));
			if (gacopyz_client_sockname(ctx, &addr, &len) == 0)
				set_milter_client_address(md->env, &addr, len);

			xeval_begin(md->env);
		}
	}
	if (!md->msgid[0]) {
		/* FIXME: Cannot use env_get_macro here, because it
		   would create a recursion. */
		const char *p = gacopyz_getsymval(ctx, "i");
		if (p) {
			size_t len = strlen(p);
			if (len > sizeof md->msgid - 3)
				len = sizeof md->msgid - 3;
			memcpy(md->msgid, p, len);
			md->msgid[len++] = ':';
			md->msgid[len++] = ' ';
			md->msgid[len] = 0;
		}
	}
	return md;
}

const char *
mailfromd_msgid(SMFICTX *ctx)
{
	struct message_data *md = priv_get(ctx);
	return md->msgid;
}


/* Run-time execution */

static const char *
ctx_getsym(void *data, const char *str)
{
	const char *ret = gacopyz_getsymval(data, str);
	if (!ret) {
		struct message_data *md = priv_get(data);
		if (strcmp (str, "s") == 0)
			ret = md->helostr;
	}
	return ret;
}

static int
ctx_setreply(void *data, char *code, char *xcode, char *message)
{
	if (code)
		return gacopyz_setreply(data, code, xcode, message);
	return 0;
}


/* Message capturing functions */
static int capture_enabled;

void
capture_on()
{
	milter_enable_state(smtp_state_helo);
	milter_enable_state(smtp_state_envfrom);
	milter_enable_state(smtp_state_header);
	milter_enable_state(smtp_state_eoh);
	milter_enable_state(smtp_state_body);
	milter_enable_state(smtp_state_eom);
	capture_enabled = 1;
}

static void
capture_from(eval_environ_t env, const char *str)
{
	if (capture_enabled) {
		time_t t;
		struct tm *tm;
		char datebuf[26];

		env_capture_start(env);

		t = time(NULL);
		tm = localtime(&t);
		mu_strftime(datebuf, sizeof datebuf,
			    "%a %b %d %H:%M:%S %Y", tm);

		env_capture_write_args(env,
				       "From ", str, " ", datebuf, "\n", NULL);
	}
}

static void
capture_header(eval_environ_t env, const char *hf, const char *hv)
{
	env_capture_write_args(env, hf, ": ", hv, "\n", NULL);
}

static void
capture_eoh(eval_environ_t env)
{
	env_capture_write_args(env, "\n", NULL);
}

static void
capture_body(eval_environ_t env, unsigned char *bodyp, size_t len)
{
	env_capture_write(env, (char*) bodyp, len);
}

static void
capture_eom(eval_environ_t env)
{
}


/* Cleanup functions */
void
filter_cleanup(SMFICTX *ctx)
{
	struct message_data *md = priv_get(ctx);
	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE9, ("cleaning up"));
	if (md) {
		env_init(md->env);
		xeval(md->env, smtp_state_end);
		free(md->helostr);
		destroy_environment(md->env);
		free(md);
		gacopyz_setpriv(ctx, NULL);
	}
}


/* Milter interface functions */

int
xeval(eval_environ_t env, enum smtp_state tag)
{
	int rc;

	env_make_frame(env);
	set_milter_state(env, tag);
	rc = eval_environment(env, entry_point[tag]);
	if (rc)
		mu_error(_("execution of the filter program was not finished"));
	return rc;
}

int
xeval_begin(eval_environ_t env)
{
	int rc = xeval(env, smtp_state_begin);
	env_save_catches(env);
	return rc;
}

sfsistat
mlfi_eval(SMFICTX *ctx, enum smtp_state tag)
{
	int rc;
	sfsistat status;
	struct message_data *md = priv_get(ctx);

	rc = xeval(md->env, tag);
	if (rc == 0)
		status = environment_get_status(md->env);
	else {
		gacopyz_setreply(ctx, "410", NULL,
			      "Local configuration error; please try again later");
		status = SMFIS_TEMPFAIL;
	}
	log_status(status, ctx);
	return status;
}

sfsistat
mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);
	int port;
	char *addrstr;
	int family;

	if (!hostaddr) {
		family = MFAM_STDIO;
		port = 0;
		addrstr = "";
		env_save_socket_family(md->env, AF_INET);
	} else {
		switch (hostaddr->sa.sa_family) {
		case PF_INET:
			family = MFAM_INET;
			port = ntohs(hostaddr->sin.sin_port);
			addrstr = inet_ntoa(hostaddr->sin.sin_addr);
			break;

		case PF_INET6: {
			char host[NI_MAXHOST];

			family = MFAM_INET6;
			port = ntohs(hostaddr->sin6.sin6_port);
			if (getnameinfo(&hostaddr->sa, sizeof(hostaddr->sin6),
					host, sizeof host,
					NULL, 0,
					NI_NUMERICHOST) == 0)
				addrstr = host;
			else
				addrstr = "[unresolved]";
			break;
		}

		case PF_UNIX:
			family = MFAM_UNIX;
			port = 0;
			addrstr = hostaddr->sunix.sun_path;
			break;

		default:
			mu_error(_("mlfi_connect: unsupported address family: %d"),
				 hostaddr->sa.sa_family);
			gacopyz_setreply(ctx, "410", NULL,
					 "Local configuration error; please try again later");
			return SMFIS_TEMPFAIL;
		}
		env_save_socket_family(md->env, hostaddr->sa.sa_family);
	}

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7,
		 ("Processing xxfi_connect: %s, %d, %s, %u",
		  hostname, family, addrstr, port));
	mf_proctitle_format("Connect: %s, %d, %s, %u",
			    hostname, family, addrstr, port);

	env_init(md->env);
	env_push_string(md->env, addrstr);
	env_push_number(md->env, port);
	env_push_number(md->env, family);
	env_push_string(md->env, hostname);
	status = mlfi_eval(ctx, smtp_state_connect);
	adjust_stack(md->env, 4);
	return status;
}

sfsistat
mlfi_helo(SMFICTX *ctx, char *helohost)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7,
		 ("Processing xxfi_helo: %s", helohost));
	mf_proctitle_format("%sHELO %s", md->msgid, helohost);

	if (md->helostr)
	  free(md->helostr);
	md->helostr = mu_strdup(helohost);
	env_init(md->env);
	env_push_string(md->env, md->helostr);
	status = mlfi_eval(ctx, smtp_state_helo);
	adjust_stack(md->env, 1);
	return status;
}

static char *
concat_args(char **argv)
{
	size_t argc;
	char *p = NULL;

	for (argc = 0; argv[argc]; argc++)
		;
	mu_argcv_string(argc, argv, &p);
	return p;
}

sfsistat
mlfi_envfrom(SMFICTX *ctx, char **argv)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);
	char *p = concat_args(argv + 1);

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7,
	      ("Processing xxfi_envfrom: %s %s", argv[0], p));
	mf_proctitle_format("%sMAIL FROM %s %s", md->msgid, argv[0], p);

	env_init(md->env);
	capture_from(md->env, argv[0]);
	env_push_string(md->env, p);
	free(p);
	env_push_string(md->env, argv[0]);
	status = mlfi_eval(ctx, smtp_state_envfrom);
	adjust_stack(md->env, 2);
	return status;
}

sfsistat
mlfi_envrcpt(SMFICTX *ctx, char ** argv)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);
	char *p = concat_args(argv + 1);

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7,
		 ("Processing xxfi_envrcpt: %s %s", argv[0], p));
	mf_proctitle_format("%sRCPT TO %s %s", md->msgid, argv[0], p);
	env_init(md->env);
	env_push_string(md->env, p);
	free(p);
	env_push_string(md->env, argv[0]);
	incr_rcpt_count(md->env);
	status = mlfi_eval(ctx, smtp_state_envrcpt);
	adjust_stack(md->env, 2);
	return status;
}

sfsistat
mlfi_data(SMFICTX *ctx)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7, ("Processing xxfi_data:"));
	mf_proctitle_format("%sDATA", md->msgid);
	env_init(md->env);
	status = mlfi_eval(ctx, smtp_state_data);
	adjust_stack(md->env, 1);
	return status;
}

sfsistat
mlfi_header(SMFICTX *ctx, char *headerf, char *headerv)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7,
		 ("Processing xxfi_header:"));
	mf_proctitle_format("%sHeader", md->msgid);
	env_init(md->env);
	capture_header(md->env, headerf, headerv);
	env_push_string(md->env, headerv);
	env_push_string(md->env, headerf);
	status = mlfi_eval(ctx, smtp_state_header);
	adjust_stack(md->env, 2);
	return status;
}

sfsistat
mlfi_eoh(SMFICTX *ctx)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);
	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7, ("Processing xxfi_eoh"));
	mf_proctitle_format("%sEOH", md->msgid);
	env_init(md->env);
	capture_eoh(md->env);
	status = mlfi_eval(ctx, smtp_state_eoh);
	adjust_stack(md->env, 0);
	return status;
}

sfsistat
mlfi_body(SMFICTX *ctx, unsigned char *bodyp, size_t len)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);
	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7,
		 ("Processing xxfi_body: %lu", (unsigned long) len));
	mf_proctitle_format("%sBODY", md->msgid);
	env_init(md->env);
	capture_body(md->env, bodyp, len);
	env_push_number(md->env, len);
	/* Push bodyp as generic pointer to avoid unnecessary stack allocation.
	   User can then convert it to string using the body_string() call.
	 */
	env_push_pointer(md->env, bodyp);
	status = mlfi_eval(ctx, smtp_state_body);
	adjust_stack(md->env, 2);
	return status;
}

size_t
mem_search(const char *str, int c, size_t size)
{
	const char *p;

	if (size == 0)
		return 0;
	p = memchr(str, c, size);
	if (p)
		return p - str;
	return size;
}

int
xlate_and_replace_body(SMFICTX *ctx, const char *value, size_t size)
{
	int rc;
	mu_opool_t pool;

	if ((rc = mu_opool_create(&pool, MU_OPOOL_DEFAULT))) {
		mu_error(_("cannot create opool: %s"), mu_strerror(rc));
		return 1;
	}

	while (size) {
		size_t n = mem_search(value, '\n', size);
		size_t off;

		if (value[n] == '\n') {
			off = n + 1;
			if (n > 0 && value[n-1] == '\r')
				n--;
		} else
			off = n;

		if ((rc = mu_opool_append(pool, value, n)) != 0
		    || (rc = mu_opool_append(pool, "\r\n", 2)) != 0) {
			mu_error(_("failed to append to opool: %s"),
				 mu_strerror(rc));
			break;
		}

		value += off;
		size -= off;
	}

	if (rc == 0) {
		mu_iterator_t itr;
		rc = mu_opool_get_iterator(pool, &itr);
		if (rc)	{
			mu_error(_("%s failed: %s"),
				 "mu_opool_iterator_create",
				 mu_strerror(rc));
		} else {
			for (mu_iterator_first (itr);
			     !mu_iterator_is_done (itr);
			     mu_iterator_next (itr)) {
				const char *ptr;
				size_t len;
				mu_iterator_current_kv (itr,
							(const void**)&len,
							(void **)&ptr);
				rc = gacopyz_replace_body(ctx,
						    (const unsigned char*)ptr,
							  len);
				if (rc) {
					mu_error(_("%s failed: %s"),
						 "gacopyz_replace_body",
						 mu_strerror(errno));
					break;
				}
			}
			mu_iterator_destroy (&itr);
		}
	}
	mu_opool_destroy (&pool);
	return rc;
}

static int
run_msgmod(void *item, void *data)
{
	struct msgmod_closure *hdr = item;
	SMFICTX *ctx = data;

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE6,
		 ("%s %s: %s %u",
		  msgmod_opcode_str(hdr->opcode),
		  SP(hdr->name), SP(hdr->value), hdr->idx));

	switch (hdr->opcode) {
	case header_add:
		gacopyz_add_header(ctx, hdr->name, hdr->value);
		break;

	case header_replace:
		gacopyz_change_header(ctx, hdr->idx, hdr->name, hdr->value);
		break;

	case header_delete:
		gacopyz_change_header(ctx, hdr->idx, hdr->name, NULL);
		break;

	case header_insert:
		gacopyz_insert_header(ctx, hdr->idx, hdr->name, hdr->value);
		break;

	case rcpt_add:
		gacopyz_add_rcpt(ctx, hdr->name);
		break;

	case rcpt_delete:
		gacopyz_del_rcpt(ctx, hdr->name);
		break;

	case quarantine:
		gacopyz_quarantine(ctx, hdr->name);
		break;

	case body_repl:
		xlate_and_replace_body(ctx, hdr->value, strlen(hdr->value));
		break;

	case body_repl_fd:
		if (gacopyz_replace_body_fd(ctx, hdr->idx) != MI_SUCCESS) {
			mu_error(_("%s failed: %s"),
				 "gacopyz_replace_body_fd",
				 mu_strerror(errno));
			break;
		}
		break;

	case set_from:
		gacopyz_chgfrom(ctx, hdr->name, hdr->value);
		break;

	default:
		abort();
	}
	return 0;
}

sfsistat
mlfi_eom(SMFICTX *ctx)
{
	sfsistat status;
	struct message_data *md = priv_get(ctx);

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7, ("Processing xxfi_eom"));
	mf_proctitle_format("%sEOM", md->msgid);

	env_init(md->env);
	capture_eom(md->env);
	status = mlfi_eval(ctx, smtp_state_eom);
	adjust_stack(md->env, 0);
	if ((status == SMFIS_ACCEPT || status == SMFIS_CONTINUE)
	    && env_msgmod_count(md->env) > 0) {
		mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE6,
			 ("flushing message modification queue"));
		env_msgmod_apply(md->env, run_msgmod, ctx);
	}

	mf_proctitle_format("%sfinished", md->msgid);
	clear_rcpt_count(md->env);
	env_msgmod_clear(md->env);

	return status;
}

sfsistat
mlfi_abort(SMFICTX *ctx)
{
	struct message_data *md = priv_get(ctx);

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7, ("Abort"));
	mf_proctitle_format("%saborting", md->msgid);
	env_msgmod_clear(md->env);
	md->msgid[0] = 0;
	/* Note: the value of helostr is not discarded: RFC 2822,
	   section 4.1.1.5 states that RSET issued immediately
	   after EHLO is effectively equivalent to a NOOP. */
	env_init_dataseg(md->env);
	return SMFIS_CONTINUE;
}

sfsistat
mlfi_close(SMFICTX *ctx)
{
	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7, ("Close"));
	mf_proctitle_format("closing");
	filter_cleanup(ctx);
	return SMFIS_CONTINUE;
}

static int
child_start()
{
	signal(SIGPIPE, SIG_IGN);
	signal(SIGALRM, SIG_IGN);
	mf_proctitle_format("startup");
	return 0;
}

sfsistat
mlfi_negotiate(SMFICTX *ctx,
	       unsigned long mta_actions,
	       unsigned long mta_capa,
	       unsigned long unused1,
	       unsigned long unused2,
	       unsigned long *filter_actions,
	       unsigned long *filter_capa,
	       unsigned long *unused4,
	       unsigned long *unused5)
{
	enum gacopyz_stage i;

	mu_debug(MF_SOURCE_ENGINE, MU_DEBUG_TRACE7,
		 ("Negotiate: mta_actions=%#lx, mta_capa=%#lx, "
		  "filter_actions=%#lx, filter_capa=%#lx",
		  mta_actions, mta_capa, *filter_actions, *filter_capa));

	/*
	 * First entered sate is connect, because smfilter.xxfi_connect is
	 * always initialized.  This means "begin" handler (if defined) will
	 * be run from this state.  Traditionally the "i" macro was
	 * available in all handlers, therefore it must be registered for
	 * that state.
	 */
	register_macro(smtp_state_connect, "i");

	for (i = 0; i < gacopyz_stage_max; i++) {
		char *str = get_stage_macro_string(i);
		gacopyz_setsymlist(ctx, i, str);
		free(str);
	}
	return SMFIS_CONTINUE;
}

static struct smfiDesc smfilter =
{
	"MailfromFilter",
	SMFI_VERSION,
	SMFI_V2_ACTS|SMFIF_CHGFROM, /* FIXME: Add flags as needed */
	mlfi_connect,	/* connection info filter */
	NULL,		/* SMTP HELO command filter */
	NULL,		/* envelope sender filter */
	NULL,		/* envelope recipient filter */
	NULL,		/* header filter */
	NULL,           /* end of header */
	NULL,		/* body block filter */
	mlfi_eom,	/* end of message */
	mlfi_abort,	/* message aborted */
	mlfi_close,	/* connection cleanup */
#ifdef GACOPYZ_VERSION_MAJOR
	NULL,   /* unknown command handler */
	NULL,   /* data handler */
	mlfi_negotiate,   /* negotiate */

	child_start,   /* child start */
	NULL,   /* child finish */
	NULL,   /* idle callback */
	NULL    /* Accept connection callback */
#endif
};


/* Milter server functions */

static void
setprocid(const char *id)
{
	char *tag;

	logger_close();
	tag = mu_alloc(strlen(mu_log_tag) + 1 + strlen(id) + 1);
	strcpy(tag, mu_log_tag);
	strcat(tag, "#");
	strcat(tag, id);
	mu_log_tag = tag;
	mf_server_log_setup();
}

int
milter_session_server(const char *id, int fd,
		      struct sockaddr const *sa, socklen_t len,
		      void *server_data, void *srvman_data)
{
	setprocid(id);
	gacopyz_context_loop(fd, &smfilter, (milter_sockaddr_t*) sa, len,
			     (void*) id);
	return 0;
}

int
mfd_callout_session_server(const char *id, int fd,
			   struct sockaddr const *sa, socklen_t len,
			   void *server_data, void *srvman_data)
{
	setprocid(id);
	return callout_session_server(id, fd, sa, len,
				      server_data, srvman_data);
}

void
milter_setlogmask(int mask)
{
	smfilter.logmask = mask;
}

void
milter_settimeout(time_t t)
{
	smfilter.ctx_timeout.tv_sec = t;
	smfilter.ctx_timeout.tv_usec = 0;
}


void
milter_enable_state(enum smtp_state state)
{
	switch (state) {
	case smtp_state_connect:
		smfilter.xxfi_connect = mlfi_connect;
		break;

	case smtp_state_helo:
		smfilter.xxfi_helo = mlfi_helo;
		break;

	case smtp_state_envfrom:
		smfilter.xxfi_envfrom = mlfi_envfrom;
		break;

	case smtp_state_envrcpt:
		smfilter.xxfi_envrcpt = mlfi_envrcpt;
		break;

	case smtp_state_data:
		smfilter.xxfi_data = mlfi_data;
		break;

	case smtp_state_header:
		smfilter.xxfi_header = mlfi_header;
		break;

	case smtp_state_eoh:
		smfilter.xxfi_eoh = mlfi_eoh;
		break;

	case smtp_state_body:
		smfilter.xxfi_body = mlfi_body;
		break;

	default:
		break;
	}
}
