/*
 * dspam backend for dovecot antispam plugin
 *
 * Copyright (C) 2004-2007, 2025  Johannes Berg <johannes@sipsolutions.net>
 *                    2006  Frank Cusack
 *
 * 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.
 */

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <fcntl.h>

#include "lib.h"
#include "mail-storage-private.h"

#include "antispam-plugin.h"


static int call_dspam(const struct antispam_settings *cfg,
		      const char *signature, enum classification wanted)
{
	pid_t pid;
	const char *class_arg;
	const char *sign_arg;
	int pipes[2];

	sign_arg = t_strconcat("--signature=", signature, NULL);
	switch (wanted) {
	case CLASS_NOTSPAM:
		class_arg = t_strconcat("--class=", "innocent", NULL);
		break;
	case CLASS_SPAM:
		class_arg = t_strconcat("--class=", "spam", NULL);
		break;
	}

	/*
	 * For dspam stderr; dspam seems to not always exit with a
	 * non-zero exit code on errors so we treat it as an error
	 * if it logged anything to stderr.
	 */
	if (pipe(pipes) < 0)
		return -1;

	pid = fork();
	if (pid < 0)
		return -1;

	if (pid) {
		int status;
		char buf[1025];
		int readsize;
		bool error = FALSE;

		close(pipes[1]);

		do {
			readsize = read(pipes[0], buf, sizeof(buf) - 1);
			if (readsize < 0) {
				readsize = -1;
				if (errno == EINTR)
					readsize = -2;
			}

			/*
			 * readsize > 0 means that we read a message from
			 * dspam, -1 means we failed to read for some odd
			 * reason
			 */
			if (readsize > 0 || readsize == -1)
				error = TRUE;

			if (readsize > 0) {
				buf[readsize] = '\0';
				debug(cfg, "dspam error: %s\n", buf);
			}
		} while (readsize == -2 || readsize > 0);

		/*
		 * Wait for dspam, should return instantly since we've
		 * already waited above (waiting for stderr to close)
		 */
		waitpid(pid, &status, 0);
		if (!WIFEXITED(status))
			error = TRUE;

		close(pipes[0]);
		if (error)
			return 1;
		return WEXITSTATUS(status);
	} else {
		int fd = open("/dev/null", O_RDONLY);
		char **argv;
		/* 4 fixed args, extra args, terminating NULL */
		int sz = sizeof(char *) * (4 + cfg->dspam_args_num + 1);
		int i;

		argv = i_malloc(sz);
		memset(argv, 0, sz);

		close(0);
		close(1);
		close(2);
		/* see above */
		close(pipes[0]);

		if (dup2(pipes[1], 2) != 2)
			exit(1);
		if (dup2(pipes[1], 1) != 1)
			exit(1);
		close(pipes[1]);

		if (dup2(fd, 0) != 0)
			exit(1);
		close(fd);

		argv[0] = (char *)cfg->dspam_binary;
		argv[1] = "--source=error";
		argv[2] = (char *)class_arg;
		argv[3] = (char *)sign_arg;

		for (i = 0; i < cfg->dspam_args_num; i++)
			argv[i + 4] = (char *)cfg->dspam_args[i];

		/*
		 * not good with stderr debuggin since we then write to
		 * stderr which our parent takes as a bug
		 */
		debugv_not_stderr(cfg, argv);

		T_BEGIN {
		for (i = 0; cfg->dspam_env && cfg->dspam_env[i]; i++) {
			char *name, *value;
			name = t_strdup_noconst(cfg->dspam_env[i]);
			value = strchr(name, '=');
			if (value) {
				*value = '\0';
				value++;
			}
			setenv(name, value, 1);
		}
		} T_END;

		execv(cfg->dspam_binary, argv);
		debug(cfg, "executing %s failed: %d (uid=%d, gid=%d)",
			cfg->dspam_binary, errno, getuid(), getgid());
		/* fall through if dspam can't be found */
		exit(127);
		/* not reached */
		return -1;
	}
}

struct antispam_transaction_context {
	struct siglist *siglist;
};

static struct antispam_transaction_context *
backend_start(const struct antispam_settings *cfg ATTR_UNUSED,
	      struct mailbox *box ATTR_UNUSED)
{
	struct antispam_transaction_context *ast;

	ast = i_new(struct antispam_transaction_context, 1);
	ast->siglist = NULL;
	return ast;
}

static void backend_rollback(const struct antispam_settings *cfg ATTR_UNUSED,
			     struct antispam_transaction_context *ast)
{
	signature_list_free(&ast->siglist);
	i_free(ast);
}

static int backend_commit(const struct antispam_settings *cfg,
			  struct mailbox_transaction_context *ctx,
			  struct antispam_transaction_context *ast)
{
	struct siglist *item = ast->siglist;
	int ret = 0;

	while (item) {
		if (call_dspam(cfg, item->sig, item->wanted)) {
			ret = -1;
			mail_storage_set_error(ctx->box->storage,
					       MAIL_ERROR_NOTPOSSIBLE,
					       "Failed to call dspam");
			break;
		}
		item = item->next;
	}

	signature_list_free(&ast->siglist);
	i_free(ast);
	return ret;
}

static int backend_handle_mail(const struct antispam_settings *cfg,
			       struct mailbox_transaction_context *t,
			       struct antispam_transaction_context *ast,
			       struct mail *mail, enum classification want)
{
	const char *const *result = NULL;
	int i;

	/*
	 * Check for whitelisted classifications that should
	 * be ignored when moving a mail. eg. virus.
	 */
	if (cfg->dspam_result_header)
		result = get_mail_headers(mail, cfg->dspam_result_header);
	if (result && result[0]) {
		for (i = 0; cfg->dspam_result_blacklist && cfg->dspam_result_blacklist[i]; i++) {
			if (strcasecmp(result[0], cfg->dspam_result_blacklist[i]) == 0)
				return 0;
		}
	}

	return signature_extract_to_list(cfg, t, mail, &ast->siglist, want);
}

static bool backend_settings(struct antispam_settings *cfg, pool_t pool ATTR_UNUSED,
			     const char **error_r ATTR_UNUSED)
{
	debug(cfg, "dspam binary set to %s\n", cfg->dspam_binary);

	if (cfg->dspam_result_header) {
		debug(cfg, "dspam result set to %s\n", cfg->dspam_result_header);

		if (cfg->_dspam_result_blacklist[0]) {
			cfg->dspam_result_blacklist = p_strsplit(cfg->pool,
								 cfg->_dspam_result_blacklist,
								 ";");
			for (int i = 0; cfg->dspam_result_blacklist[i]; i++)
				debug(cfg, "dspam result blacklist %s\n",
				      cfg->dspam_result_blacklist[i]);
		}
	}

	if (cfg->_dspam_args[0]) {
		cfg->dspam_args = p_strsplit(cfg->pool, cfg->_dspam_args, ";");
		cfg->dspam_args_num = str_array_length((const char * const *)cfg->dspam_args);
		for (int i = 0; cfg->dspam_args[i]; i++)
			debug(cfg, "dspam extra arg %s\n", cfg->dspam_args[i]);
	}

	if (cfg->_dspam_env[0]) {
		cfg->dspam_env = p_strsplit(cfg->pool, cfg->_dspam_env, ";");
		for (int i = 0; cfg->dspam_env[i]; i++)
			debug(cfg, "dspam env %s\n", cfg->dspam_env[i]);
	}

	return true;
}

struct backend dspam_backend = {
	.settings = backend_settings,
	.handle_mail = backend_handle_mail,
	.start = backend_start,
	.rollback = backend_rollback,
	.commit = backend_commit,
};
