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

   Mailfromd 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.

   Mailfromd 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 mailfromd.  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 <termios.h>
#include <sys/ioctl.h>

#include <mailutils/mailutils.h>
#include <mailutils/server.h>
#include <mailutils/syslog.h>
#include <mailutils/cli.h>
#include <mailutils/dbm.h>

#include "mailfromd.h"
#include "callout.h"
#include "srvman.h"
#include "srvcfg.h"
#include "filenames.h"
#include "builtin.h"
#include "prog.h"


/* Configurable options */

int mode = MAILFROMD_DAEMON; /* Default operation mode */
enum smtp_state test_state = smtp_state_envfrom; /* State for --test mode */
int need_script = 1;   /* Set if the current mode requires reading the
			  script file */
char *script_file;
int script_check;      /* Check config file syntax and exit */
extern int yy_flex_debug;   /* Enable tracing the lexical analyzer */
int script_ydebug;     /* Enable tracing the parser */
int script_dump_tree;  /* Dump created config tree to stdout */
int script_dump_code;  /* Dump disassembled code to stdout */
int script_dump_macros; /* Dump used Sendmail macros */
int script_dump_xref;  /* Dump cross-reference */
int preprocess_option; /* Only preprocess the sources */
int location_column_option; /* Print column numbers in error locations */

char *ext_pp_options;
int ext_pp_options_given;

int do_trace;          /* Enable tracing configuration */
unsigned optimization_level = 1; /* Optimization level */

int stack_trace_option;          /* Print stack traces on runtime errors */

char *main_function_name = "main";
char *callout_server_url;

char *echo_output; /* Output file for 'echo' statements. */
mu_stream_t mf_strecho; /* Output stream for 'echo' statements */

#define ARG_UNSET (-1)

static int trace_option = ARG_UNSET;
static mu_list_t trace_modules;

static char *resolv_conf_file;

/* Preprocessor helper function */
static void
add_pp_option(const char *opt, const char *arg)
{
	size_t len;

	len = 1 + strlen(opt) + (arg ? strlen(arg) : 0);
	if (ext_pp_options) {
		len += strlen(ext_pp_options);
		ext_pp_options = mu_realloc(ext_pp_options, len + 1);
	} else {
		ext_pp_options = mu_alloc(len + 1);
		ext_pp_options[0] = 0;
	}
	strcat(ext_pp_options, " ");
	strcat(ext_pp_options, opt);
	strcat(ext_pp_options, arg);
}

void
pp_define(const char *symbol)
{
	add_pp_option("-D", symbol);
}


/* Logging & debugging */

static mu_stream_t mf_trace_stream;

static void
open_trace_stream()
{
	int rc = mu_filter_create(&mf_trace_stream,
				  mf_strecho,
				  "C-escape",
				  MU_FILTER_ENCODE,
				  MU_STREAM_WRITE);
	if (rc) {
		mu_error(_("cannot create trace stream, "
			   "using standard log: %s"),
			 mu_strerror(rc));
		mf_trace_stream = mf_strecho;
	} 
}

void
trace(const char *fmt, ...)
{
	if (do_trace) {
		int bval = 0;
		va_list ap;

		if (!mf_trace_stream)
			open_trace_stream();
		
		va_start(ap, fmt);
		mu_stream_vprintf(mf_trace_stream, fmt, ap);
		bval = 1;
		mu_stream_ioctl(mf_trace_stream, MU_IOCTL_FILTER,
				MU_IOCTL_FILTER_SET_DISABLED,
				&bval);
		mu_stream_write(mf_trace_stream, "\n", 1, NULL);
		bval = 0;
		mu_stream_ioctl(mf_trace_stream, MU_IOCTL_FILTER,
				MU_IOCTL_FILTER_SET_DISABLED,
				&bval);
		va_end(ap);
	}
}

void
log_status(sfsistat status, SMFICTX *ctx)
{
	mu_debug_level_t lev;

	mu_debug_category_level(NULL, 0, &lev);
	if ((lev & ~MU_DEBUG_LEVEL_MASK(MU_DEBUG_ERROR)) &&
	    status != SMFIS_CONTINUE) {
		const char *str = sfsistat_str(status);
		if (str)
			logmsg(MU_LOG_INFO,
			       "%s%s", mailfromd_msgid(ctx), str);
		else
			logmsg(MU_LOG_INFO,
			       "%sstatus %d",
			       mailfromd_msgid(ctx), status);
	}
}


/* Sendmail class file support */

static mu_list_t domain_list;

/* Read domains from sendmail-style domain file NAME and store them in
   DOMAIN_LIST */
int
read_domain_file(const char *name)
{
	FILE *fp;
	char buf[256];
	char *p;
	
	fp = fopen(name, "r");
	if (!fp) {
		mu_error(_("cannot open file `%s': %s"),
			 name, mu_strerror(errno));
		return 1;
	}

	if (!domain_list) {
		mu_list_create(&domain_list);
		mu_list_set_comparator(domain_list, mf_list_compare_string);
	}

	while (p = fgets(buf, sizeof buf, fp)) {
		char *q;
		
		while (*p && mu_isspace(*p))
			p++;
		if (*p == '#')
			continue;

		for (q = p; *q && !mu_isspace(*q); q++)
			;
		*q = 0;

		if (*p == 0)
			continue;
		
		mu_list_append(domain_list, strdup(p));
	}

	fclose(fp);
	return 0;
}

/* Return true if we relay domain NAME */
int
relayed_domain_p(char *name)
{
	char *p = name;

	while (p) {
		if (mu_list_locate(domain_list, p, NULL) == 0) {
			mu_debug(MF_SOURCE_MAIN, MU_DEBUG_TRACE5, 
				 ("%s is in relayed domain %s", name, p));
			return 1;
		}
		p = strchr(p, '.');
		if (p)
			p++;
	}
	return 0;
}

/* Return true if CLIENT represents a host we relay. CLIENT is a dotted-quad
   IP address. */
int
host_in_relayed_domain_p(char *client)
{
	int rc;
	char *hbuf;

	if (mu_list_is_empty(domain_list))
		return 0;

	if (resolve_ipstr(client, &hbuf))
		return 0;
	rc = relayed_domain_p(hbuf);
	free(hbuf);
	return rc;
}

static mu_list_t relayed_domain_files;

static int
load_relay_file(void *item, void *data)
{
	read_domain_file(item);
	return 0;
}

static void
init_relayed_domains(void)
{
	mu_list_foreach(relayed_domain_files, load_relay_file, NULL);
	mu_list_destroy(&relayed_domain_files);
}

/* Command line parsing */

const char *program_version = "mailfromd (" PACKAGE_STRING ")";
static char prog_doc[] = N_("mailfromd -- a general purpose milter daemon");
static char args_doc[] = "[var=value...][SCRIPT]";

static void
opt_testmode(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	mode = MAILFROMD_TEST;
	if (arg) {
		test_state =  string_to_state(arg);
		if (test_state == smtp_state_none) {
			mu_parseopt_error(po,
					  _("unknown smtp state tag: %s"),
					  arg);
			exit(po->po_exit_error);
		}
		log_stream = "stderr";
		need_script = 1;
	}
}

static void
opt_runmode(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	mode = MAILFROMD_RUN;
	if (arg)
		main_function_name = mu_strdup (arg);
	log_stream = "stderr";
	need_script = 1;
}

static void
opt_lint(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	log_stream = "stderr";
	script_check = 1;
	need_script = 1;
}

static void
opt_show_defaults(struct mu_parseopt *po, struct mu_option *op,
		  char const *arg)
{
	mode = MAILFROMD_SHOW_DEFAULTS;
	need_script = 0;
}

static void
opt_daemon(struct mu_parseopt *po, struct mu_option *op,
	   char const *arg)
{
	mode = MAILFROMD_DAEMON;
	need_script = 1;
}

static void
opt_path_add(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	pathbuf_add((struct pathbuf *)op->opt_ptr, arg);
}

static void
opt_milter_socket(struct mu_parseopt *po, struct mu_option *op,
		  char const *arg)
{
	mf_srvcfg_add("default", arg);
}

static void
opt_callout_socket(struct mu_parseopt *po, struct mu_option *op,
		   char const *arg)
{
	mf_srvcfg_add("callout", arg);
	free(callout_server_url);
	callout_server_url = mu_strdup(arg);
}

static void
opt_mtasim(struct mu_parseopt *po, struct mu_option *op,
	   char const *arg)
{
	mtasim_option = 1;
	server_flags |= MF_SERVER_FOREGROUND;
}

static void
opt_optimize(struct mu_parseopt *po, struct mu_option *op,
	     char const *arg)
{
	if (!arg)
		optimization_level = 1;
	else {
		char *p;
		optimization_level = strtoul(arg, &p, 0);
		if (*p)
			mu_parseopt_error(po,
					  _("-O level must be a non-negative integer"));
	}
}

static void
opt_variable(struct mu_parseopt *po, struct mu_option *op,
	     char const *arg)
{
	char *p;
	struct mu_locus_range locus = {{ "<command line>", 1, 0 }};

	p = strchr(arg, '=');
	if (!p) {
		mu_parseopt_error(po,
				  _("expected assignment, but found `%s'"),
				  arg);
		exit(po->po_exit_error);
	}
	*p++ = 0;
	defer_initialize_variable(arg, p, &locus);
}

static void
opt_relayed_domain_file(struct mu_parseopt *po, struct mu_option *op,
			char const *arg)
{
	if (!relayed_domain_files) 
		mu_list_create(&relayed_domain_files);
	mu_list_append(relayed_domain_files, mu_strdup(arg));
}

static void
opt_disable_preprocessor(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	preprocessor.enabled = 0;
}

static void
opt_preprocessor(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	preprocessor.enabled = 1;
	if (arg)
		preprocessor.command = (char*) arg;
	else if (!preprocessor.command) {
		mu_error(_("preprocessor options given, "
			   "but no default preprocessor defined: "
			   "use the --preprocessor option"));
		exit(EX_USAGE);
	}
}

static void
opt_define(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	if (preprocessor.pass_defines) {
		ext_pp_options_given = 1;
		add_pp_option("-D", arg);
	} else
		mu_error("%s",
			 _("Your configuration file disables passing defines to the preprocessor"));
}

static void
opt_undefine(struct mu_parseopt *po, struct mu_option *op, char const *arg)
{
	if (preprocessor.pass_defines) {
		ext_pp_options_given = 1;
		add_pp_option("-U", arg);
	} else
		mu_error("%s",
			 _("Your configuration file disables passing defines to the preprocessor"));
}

static void
opt_set_milter_timeout(struct mu_parseopt *po, struct mu_option *op,
		       char const *arg)
{
	time_t v;
	
	if (mu_str_to_c(arg, mu_c_time, &v, NULL)) {
		mu_parseopt_error(po, _("%s: not a valid interval"), arg);
		exit(po->po_exit_error);
	}

	if (smfi_settimeout(v) == MI_FAILURE) {
		mu_parseopt_error(po, _("invalid milter timeout: %s"), arg);
		exit(po->po_exit_error);
	}
}

static void
destroy_trace_item(void *ptr)
{
	free(ptr);
}

static void
opt_trace_program(struct mu_parseopt *po, struct mu_option *op,
		  char const *arg)
{
	if (!trace_modules) {
		mu_list_create(&trace_modules);
		mu_list_set_destroy_item(trace_modules, destroy_trace_item);
	}
	mu_list_append(trace_modules, mu_strdup(arg ? arg : "all"));
}

static void
opt_dump_code(struct mu_parseopt *po, struct mu_option *op,
	      char const *arg)
{
	script_dump_code = 1;
	log_stream = "stderr";
}

static void
opt_dump_grammar_trace(struct mu_parseopt *po, struct mu_option *op,
		       char const *arg)
{
	script_ydebug = 1;
	log_stream = "stderr";
}

static void
opt_dump_lex_trace(struct mu_parseopt *po, struct mu_option *op,
		       char const *arg)
{
	yy_flex_debug = 1;
	log_stream = "stderr";
}

static void
opt_dump_macros(struct mu_parseopt *po, struct mu_option *op,
		char const *arg)
{
	script_dump_macros = 1;
	log_stream = "stderr";
}

static void
opt_dump_tree(struct mu_parseopt *po, struct mu_option *op,
	      char const *arg)
{
	script_dump_tree = 1;
	log_stream = "stderr";
}

static void
opt_gacopyz_log(struct mu_parseopt *po, struct mu_option *op,
		char const *arg)
{
	int lev = gacopyz_string_to_log_level(arg);
	if (lev == -1) {
		mu_parseopt_error(po, _("%s: invalid log level"), arg);
		exit(po->po_exit_error);
	}
	milter_setlogmask(SMI_LOG_FROM(lev));
}

static void
opt_dump_xref(struct mu_parseopt *po, struct mu_option *op,
	      char const *arg)
{
	script_dump_xref = 1;
	log_stream = "stderr";
}

static struct mu_option mailfromd_options[] = {
	MU_OPTION_GROUP(N_("Operation modifiers")),
	{ "test", 't', N_("HANDLER"), MU_OPTION_ARG_OPTIONAL,
	  N_("run in test mode"),
	  mu_c_string, NULL, opt_testmode },
	{ "run",    0, N_("FUNC"), MU_OPTION_ARG_OPTIONAL,
	  N_("run script and execute function FUNC (\"main\" if not given)"),
	  mu_c_string, NULL, opt_runmode },
	{ "lint",   0, NULL, MU_OPTION_DEFAULT,
	  N_("check syntax and exit"),
	  mu_c_string, NULL, opt_lint },
	{ "syntax-check", 0, NULL, MU_OPTION_ALIAS, },
	{ "show-defaults", 0, NULL, MU_OPTION_DEFAULT,
	  N_("show compilation defaults"),
	  mu_c_string, NULL, opt_show_defaults },
	
	{ "daemon", 0, NULL, MU_OPTION_DEFAULT,
	  N_("run in daemon mode (default)"),
	  mu_c_string, NULL, opt_daemon },

	{ NULL, 'E', NULL, MU_OPTION_DEFAULT,
	  N_("preprocess source files and exit"),
	  mu_c_bool, &preprocess_option },
	/* Reserved for future use: */
	{ "compile", 'c', NULL, MU_OPTION_HIDDEN,
	  N_("compile files"),
	  mu_c_void },
	{ "load", 'l', N_("FILE"), MU_OPTION_HIDDEN, 
	  N_("load library"),
	  mu_c_void },
	{ "load-dir", 'L', N_("DIR"), MU_OPTION_HIDDEN,
	  N_("add DIR to the load path"),
	  mu_c_void },

	MU_OPTION_GROUP(N_("General options")),
	{ "include-path", 'I', N_("DIR"), MU_OPTION_DEFAULT,
	  N_("add the directory DIR to the list of directories to be "
	     "searched for header files"),
	  mu_c_string, &include_path, opt_path_add },
	{ "module-path", 'P', N_("DIR"), MU_OPTION_DEFAULT,
	  N_("add the directory DIR to the list of directories to be "
	     "searched for MFL module files"),
	  mu_c_string, &module_path, opt_path_add },
	{ "port", 'p', N_("STRING"), MU_OPTION_DEFAULT,
	  N_("set communication socket"),
	  mu_c_string, NULL, opt_milter_socket },
	{ "milter-socket", 0, NULL, MU_OPTION_ALIAS },	
	{ "callout-socket", 0, N_("STRING"), MU_OPTION_DEFAULT,
	  N_("set callout socket"),
	  mu_c_string, NULL, opt_callout_socket },
	{ "mtasim", 0, NULL, MU_OPTION_IMMEDIATE,
	  N_("run in mtasim compatibility mode"),
	  mu_c_string, NULL, opt_mtasim },
	{ "optimize", 'O', N_("LEVEL"), MU_OPTION_ARG_OPTIONAL,
	  N_("set code optimization level"),
	  mu_c_string, NULL, opt_optimize },
	{ "variable", 'v', N_("VAR=VALUE"), MU_OPTION_DEFAULT,
	  N_("assign VALUE to VAR"),
	  mu_c_string, NULL, opt_variable },
	{ "relayed-domain-file", 0, N_("FILE"), MU_OPTION_DEFAULT,
	  N_("read relayed domains from FILE"),
	  mu_c_string, NULL, opt_relayed_domain_file },
	{ "resolv-conf-file", 0, N_("FILE"), MU_OPTION_DEFAULT,
	  N_("read resolver configuration from FILE"),
	  mu_c_string, &resolv_conf_file },
	{ "echo", 0, N_("FILE"), MU_OPTION_ARG_OPTIONAL,
	  N_("divert output of the \"echo\" instruction to FILE"),
	  mu_c_string, &echo_output, NULL, "-" },

	MU_OPTION_GROUP(N_("Preprocessor options")), 
	{ "preprocessor", 0, N_("COMMAND"), MU_OPTION_ARG_OPTIONAL,
	  N_("use command as external preprocessor"),
	  mu_c_string, NULL, opt_preprocessor },
	{ "no-preprocessor", 0, NULL, MU_OPTION_DEFAULT,
	  N_("disable the use of external preprocessor"),
	  mu_c_string, NULL, opt_disable_preprocessor },
	{ "define", 'D', N_("NAME[=VALUE]"), MU_OPTION_DEFAULT,
	  N_("define a preprocessor symbol NAME as having VALUE, or empty"),
	  mu_c_string, NULL, opt_define },
	{ "undefine", 'U', N_("NAME"), MU_OPTION_DEFAULT,
	  N_("undefine a preprocessor symbol NAME"),
	  mu_c_string, NULL, opt_undefine },

	MU_OPTION_GROUP(N_("Timeout control")),
	{ "milter-timeout", 0, N_("TIME"), MU_OPTION_DEFAULT,
	  N_("set MTA connection timeout"),
	  mu_c_string, NULL, opt_set_milter_timeout },
	
	MU_OPTION_GROUP (N_("Informational and debugging options")),
	{ "location-column", 0, NULL, MU_OPTION_DEFAULT,
	  N_("print location column numbers in compiler diagnostics messages"),
	  mu_c_bool, &location_column_option },
	{ "trace", 0, NULL, MU_OPTION_DEFAULT,
	  N_("trace executed actions"),
	  mu_c_bool, &trace_option },
	{ "trace-program", 0, N_("MODULES"), MU_OPTION_ARG_OPTIONAL,
	  N_("enable filter program tracing"),
	  mu_c_string, NULL, opt_trace_program },
	
	{ "dump-code", 0, NULL, MU_OPTION_DEFAULT,
	  N_("dump disassembled code"),
	  mu_c_string, NULL, opt_dump_code },
	
	{ "dump-grammar-trace", 0, NULL, MU_OPTION_DEFAULT,
	  N_("dump grammar traces"),
	  mu_c_string, NULL, opt_dump_grammar_trace },
	{ "dump-lex-trace", 0, NULL, MU_OPTION_DEFAULT,
	  N_("dump lexical analyzer traces"),
	  mu_c_string, NULL, opt_dump_lex_trace },
	{ "dump-tree", 0, NULL, MU_OPTION_DEFAULT,
	  N_("dump parser tree"),
	  mu_c_string, NULL, opt_dump_tree },
	{ "dump-macros", 0, NULL, MU_OPTION_DEFAULT,
	  N_("show used Sendmail macros"),
	  mu_c_string, NULL, opt_dump_macros },
	{ "xref", 0, NULL, MU_OPTION_DEFAULT,
	  N_("produce a cross-reference listing"),
	  mu_c_string, NULL, opt_dump_xref },
	{ "dump-xref", 0, NULL, MU_OPTION_ALIAS },
	{ "gacopyz-log", 0, N_("LEVEL"), MU_OPTION_DEFAULT,
	  N_("set Gacopyz log level"),
	  mu_c_string, NULL, opt_gacopyz_log },
	{ "stack-trace", 0, NULL, MU_OPTION_DEFAULT,
	  N_("enable stack traces on runtime errors"),
	  mu_c_bool, &stack_trace_option },

	MU_OPTION_END
}, *options[] = { mailfromd_options, NULL };

static int
validate_options()
{
	/* FIXME */
	return 0;
}

static int
flush_trace_module(void *item, void *data)
{
	enable_prog_trace((const char *) item);
	return 0;
}

static char *capa[] = {
	"auth",
	"debug",
	"logging",
	"locking",
	"mailer",
	".mfd:server",
	NULL
};


/* Mailutils configuration */

static int
cb_milter_timeout(void *data, mu_config_value_t *arg)
{
	struct timeval tv;
	int rc = config_cb_timeout (&tv, arg);

	if (rc)
		return 1;
	if (smfi_settimeout(tv.tv_sec) == MI_FAILURE) {
		mu_error(_("Invalid milter timeout: %lu"),
			 (unsigned long) tv.tv_sec);
		exit(EX_USAGE);
	}
	
	return 0;
}

static int
cb_set_variable(void *data, mu_config_value_t *arg)
{
	const char *value;
	char *alloc_str = NULL;
	struct mu_locus_range locus = MU_LOCUS_RANGE_INITIALIZER;

	if (mu_cfg_assert_value_type(arg, MU_CFG_ARRAY))
		return 1;
	if (arg->v.arg.c < 2) {
		mu_error(_("not enough arguments"));
		return 1;
	} else if (arg->v.arg.c > 2) {
		mu_error(_("too many arguments"));
		return 1;
	}

	if (mu_cfg_assert_value_type(&arg->v.arg.v[0], MU_CFG_STRING))
		return 1;
	
	switch (arg->v.arg.v[1].type) {
	case MU_CFG_STRING:
		value = arg->v.arg.v[1].v.string;
		break;

	case MU_CFG_ARRAY:
		alloc_str = config_array_to_string(&arg->v.arg.v[1]);
		value = alloc_str;
		break;

	case MU_CFG_LIST:
		mu_error(_("unexpected list"));
		return 1;

	default:
		mu_error (_("INTERNAL ERROR at %s:%d: please report"),
			  __FILE__, __LINE__);
		abort();
	}

	mu_stream_ioctl (mu_strerr, MU_IOCTL_LOGSTREAM,
			 MU_IOCTL_LOGSTREAM_GET_LOCUS_RANGE, &locus);
	defer_initialize_variable(arg->v.arg.v[0].v.string, value, &locus);
	mu_locus_range_deinit(&locus);
	free(alloc_str);
	return 0;
}

static int
cb_pathbuf_add(void *data, mu_config_value_t *val)
{
	int i, rc = 0;
	mu_iterator_t itr;
	struct pathbuf *pb = data;
	
	switch (val->type) {
	case MU_CFG_STRING:
		pathbuf_add(pb, val->v.string);
		break;

	case MU_CFG_ARRAY:
		for (i = 0; i < val->v.arg.c; i++) {
			if (mu_cfg_assert_value_type(&val->v.arg.v[i],
						     MU_CFG_STRING))
				return 1;
			pathbuf_add(pb, val->v.arg.v[i].v.string);
		}
		break;

	case MU_CFG_LIST:
		mu_list_get_iterator(val->v.list, &itr);
		for (mu_iterator_first(itr);
		     !mu_iterator_is_done(itr); mu_iterator_next(itr)) {
			mu_config_value_t *pval;
			mu_iterator_current(itr, (void*) &pval);
			rc = mu_cfg_assert_value_type(pval, MU_CFG_STRING);
			if (rc)
				break;
			pathbuf_add(pb, pval->v.string);
		}
		mu_iterator_destroy(&itr);
	}
	return rc;
}

static int
cb_trace_program(void *data, mu_config_value_t *val)
{
	int i, rc = 0;
	
	switch (val->type) {
	case MU_CFG_STRING:
		enable_prog_trace(val->v.string);
		break;

	case MU_CFG_ARRAY:
		for (i = 0; i < val->v.arg.c; i++) {
			if (mu_cfg_assert_value_type(&val->v.arg.v[i],
						     MU_CFG_STRING))
				return 1;
			enable_prog_trace(val->v.arg.v[i].v.string);
		}
		break;
		
	case MU_CFG_LIST:
		mu_list_foreach(val->v.list, flush_trace_module, NULL);
		break;
	}
	return rc;
}

static int
cfg_load_relay_file(void *item, void *data)
{
	mu_config_value_t *val = item;
	if (mu_cfg_assert_value_type(val, MU_CFG_STRING) == 0)
		read_domain_file(val->v.string);
	return 0;
}

static int
cb_relayed_domain_file(void *data, mu_config_value_t *val)
{
	switch (val->type) {
	case MU_CFG_STRING:
		read_domain_file(val->v.string);
		break;

	case MU_CFG_LIST:
		mu_list_foreach(val->v.list, cfg_load_relay_file, NULL);
		break;

	default:
		mu_error (_("expected string or list of strings"));
	}
	return 0;
}


struct mu_cfg_param mf_cfg_param[] = {
 	{ ".mfd:server",  mu_cfg_section, NULL, 0, NULL, NULL },
	{ "resolver", mu_cfg_section, NULL, 0, NULL, NULL },
	{ "stack-trace", mu_c_bool, &stack_trace_option, 0, NULL,
	  N_("Dump stack traces on runtime errors.") },
	
	{ "milter-timeout", mu_cfg_callback, NULL, 0, cb_milter_timeout,
	  N_("Set milter timeout."),
	  N_("time: interval") },
        { "callout-url", mu_c_string, &callout_server_url, 0, NULL,
	  N_("Sets the URL of the default callout server."),
	  N_("url") },
	{ "include-path", mu_cfg_callback, &include_path, 0, cb_pathbuf_add,
	  N_("Add directories to the list of directories to be searched for "
	     "header files.  Argument is a list of directory names "
	     "separated by colons."),
	  N_("path: string") },
	{ "module-path", mu_cfg_callback, &module_path, 0, cb_pathbuf_add,
	  N_("Add directories to the list of directories to be searched for "
	     "MFL modules.  Argument is a list of directory names "
	     "separated by colons."),
	  N_("path: string") },
	{ "setvar", mu_cfg_callback, NULL, 0, cb_set_variable,
	  N_("Initialize a mailfromd variable <var> to <value>."),
	  N_("var: string> <value: string-or-number") },
	{ "script-file", mu_c_string, &script_file, 0, NULL,
	  N_("Read filter script from <file>."),
	  N_("file") },
	{ "trace-actions", mu_c_bool, &do_trace, 0, NULL,
	  N_("Trace executed actions.") },
	{ "trace-program", mu_cfg_callback, NULL, 0, cb_trace_program,
	  N_("Enable filter program tracing."),
	  N_("modules: list") },

	{ "relayed-domain-file", mu_cfg_callback, NULL, 0,
	  cb_relayed_domain_file,
	  N_("Read relayed domain names from the file"),
	  N_("file: string") },

	{ "preprocessor", mu_cfg_section, NULL },
	{ "runtime", mu_cfg_section, NULL },
	
	{ NULL }
};


static struct mu_cfg_param *runtime_param;
static size_t runtime_param_cnt;
static size_t runtime_param_max;

static void
_add_runtime_param_entry(struct mu_cfg_param *p)
{
	if (runtime_param_cnt == runtime_param_max) {
		if (runtime_param_max == 0)
			runtime_param_max = 16;
		runtime_param = mu_2nrealloc(runtime_param,
					   &runtime_param_max,
					   sizeof(runtime_param[0]));
	}
	runtime_param[runtime_param_cnt++] = *p;
}

void
mf_add_runtime_params(struct mu_cfg_param *p)
{
	for (; p->ident; p++)
		_add_runtime_param_entry(p);
}

void
mf_runtime_param_finish()
{
	if (runtime_param_cnt) {
		static struct mu_cfg_param term = { NULL };
		struct mu_cfg_section *section;
		
		_add_runtime_param_entry(&term);
	
		if (mu_create_canned_section ("runtime", &section) == 0) {
			section->parser = NULL;
			section->docstring = N_("Configure MFL runtime values.");
			section->label = NULL;
			mu_cfg_section_add_params(section, runtime_param);
		}
	} else {
		struct mu_cfg_param *p;
		for (p = mf_cfg_param; p->ident; p++) {
			if (strcmp (p->ident, "runtime") == 0) {
				memset (p, 0, sizeof (*p));
				break;
			}
		}
	}
}


/* Auxiliary functions */
unsigned keyword_column = 0;
unsigned header_column = 2;
unsigned value_column = 32;
unsigned right_margin = 79;

static void
set_column(mu_stream_t str, unsigned margin)
{
	mu_stream_ioctl(str, MU_IOCTL_WORDWRAPSTREAM,
			MU_IOCTL_WORDWRAP_SET_MARGIN,
			&margin);
}

static int
db_format_enumerator(struct db_format *fmt, void *data)
{
	mu_stream_t str = data;

	set_column(str, keyword_column);
        mu_stream_printf(str, "%s database:", fmt->name);
	set_column(str, value_column);
	mu_stream_printf(str, "%s\n", fmt->dbname);
	
	set_column(str, keyword_column);
	if (strcmp(fmt->name, "cache") == 0) {
		mu_stream_printf(str, "%s positive expiration:", fmt->name);
		set_column(str, value_column);
		mu_stream_printf(str, "%lu\n", fmt->expire_interval);
		
		set_column(str, keyword_column);
		mu_stream_printf(str, "%s negative expiration:", fmt->name);
		set_column(str, value_column);
		mu_stream_printf(str, "%lu\n", negative_expire_interval);
	} else {
		mu_stream_printf(str, "%s expiration:", fmt->name);
		set_column(str, value_column);
		mu_stream_printf(str, "%lu\n", fmt->expire_interval);
	}
	return 0;
}

static void
print_supported_dbtypes(mu_stream_t str)
{
	mu_iterator_t itr;
	int rc;
	const char *defdb = DEFAULT_DB_TYPE;

	rc = mu_dbm_impl_iterator(&itr);
	if (rc) {
		mu_stream_printf(str, "%s\n", _("unknown"));
		mu_error("%s", mu_strerror(rc));
	} else {
		int i;
		for (mu_iterator_first(itr), i = 0; !mu_iterator_is_done(itr);
		     mu_iterator_next(itr), i++) {
			struct mu_dbm_impl *impl;
			
			mu_iterator_current(itr, (void**)&impl);
			if (i)
				mu_stream_printf(str, ", ");
			else if (!defdb)
				defdb = impl->_dbm_name;
			mu_stream_printf(str, "%s", impl->_dbm_name);
		}
		mu_stream_write(str, "\n", 1, NULL);
		mu_iterator_destroy(&itr);
	}
//FIXME
	set_column(str, keyword_column);
	mu_stream_printf(str, "%s:", "default database type");
	set_column(str, value_column);
	mu_stream_write(str, defdb, strlen(defdb), NULL);
}

struct string_value {
	char const *kw;
	int type;
	union {
		char *s_const;
		char **s_var;
		char const *(*s_func) (void);
		void (*s_print) (mu_stream_t);
	} data;
};

char const *
string_preprocessor(void)
{
	return (preprocessor.enabled && preprocessor.command)
		  ? preprocessor.command : "none";
}

static void
print_optional_values(mu_stream_t str)
{
	static char *features[] = {
#if defined WITH_DKIM
		"DKIM",
#endif	
#if defined WITH_GEOIP2
		"GeoIP2",
#endif
#if defined WITH_MFMOD
		"mfmod",
#endif
#if defined WITH_MAILUTILS_GNUTLS
		"STARTTLS",
#endif
		NULL
	};

	if (features[0]) {
		int i;
		
		mu_stream_write(str, features[0], strlen(features[0]), NULL);
		for (i = 1; features[i]; i++) {
			mu_stream_write(str, ", ", 2, NULL);
			mu_stream_write(str, features[i], strlen(features[i]), NULL);
		}
	}
}

static void
print_script_file(mu_stream_t str)
{
	mu_stream_printf(str, "%s/%s%s",
			 SYSCONFDIR, DEFAULT_SCRIPT_FILE_STEM,
			 default_suffixes[0]);
}

#ifdef USE_SYSLOG_ASYNC	
# if DEFAULT_SYSLOG_ASYNC == 1
#  define DEFAULT_SYSLOG "non-blocking"
# else
#  define DEFAULT_SYSLOG "blocking"
# endif
#else
#  define DEFAULT_SYSLOG "blocking"
#endif

enum {
	STRING_CONSTANT,
	STRING_VARIABLE,
	STRING_FUNCTION,
	STRING_PRINTER
};

static struct string_value string_values[] = {
	{ "version",        STRING_CONSTANT, { .s_const = VERSION } },
	{ "script file",    STRING_PRINTER,  { .s_print = print_script_file } },
	{ "preprocessor",   STRING_FUNCTION,
	  { .s_func  = string_preprocessor } },
	{ "user",           STRING_VARIABLE,
	  { .s_var   = &mf_server_user } },
	{ "statedir",       STRING_VARIABLE,
	  { .s_var   = &mailfromd_state_dir } },
	{ "socket",         STRING_CONSTANT,
	  { .s_const = DEFAULT_SOCKET } },
	{ "pidfile",        STRING_VARIABLE,
	  { .s_var   = &pidfile } },
	{ "default syslog", STRING_CONSTANT,
	  { .s_const = DEFAULT_SYSLOG } },
	{ "include path",   STRING_VARIABLE,
	  { .s_var = &include_path.base } },
	{ "module path",    STRING_VARIABLE,
	  { .s_var = &module_path.base } },
#ifdef WITH_MFMOD
	{ "mfmod path",     STRING_VARIABLE, { .s_var   = &mfmod_path } },
#endif
	{ "optional features", STRING_PRINTER,
	  { .s_print = print_optional_values } },
	{ "supported database types", STRING_PRINTER,
	  { .s_print = print_supported_dbtypes } },
	{ NULL }
};

static void
print_string_values(mu_stream_t str)
{
	struct string_value *p;
	char const *val;

	for (p = string_values; p->kw; p++) {
		set_column(str, keyword_column);
		mu_stream_printf(str, "%s:", p->kw);
		set_column(str, value_column);

		switch (p->type) {
		case STRING_CONSTANT:
			val = p->data.s_const;
			break;

		case STRING_VARIABLE:
			val = *p->data.s_var;
			break;

		case STRING_FUNCTION:
			val = p->data.s_func();
			break;

		case STRING_PRINTER:
			p->data.s_print(str);
			mu_stream_write(str, "\n", 1, NULL);
			continue;
		}

		mu_stream_printf(str, "%s\n", val);
	}
}

void
mailfromd_show_defaults(void)
{
	mu_stream_t str;
	struct winsize ws;
	
	ws.ws_col = ws.ws_row = 0;
	if ((MAILUTILS_VERSION_MAJOR == 3 && MAILUTILS_VERSION_MINOR < 16) ||
	    ioctl(1, TIOCGWINSZ, (char *) &ws) < 0 || ws.ws_col == 0 ||
	    mu_wordwrap_stream_create (&str, mu_strout, 0, ws.ws_col)) {
		str = mu_strout;
		mu_stream_ref(str);
	}

	print_string_values(str);
	
	db_format_enumerate(db_format_enumerator, str);

	mu_stream_destroy(&str);
}	

static int
args_in_order(int argc, char **argv)
{
	int i;
	for (i = 0; i < argc; i++) {
		size_t len = strcspn(argv[i], "=");
		if (len > 3
		    && (memcmp(argv[i], "--ru", 4) == 0
			|| memcmp(argv[i], "--run", 5) == 0)) {
			return MF_GETOPT_IN_ORDER;
		}
	}
	return MF_GETOPT_DEFAULT;
}


static void
provide_default_milter_server(void)
{
	if (mfd_srvman_count_servers() == 0) {
		mu_diag_output(MU_DIAG_WARNING,
			       _("no servers defined; will listen on %s"),
			       DEFAULT_SOCKET);
		mf_srvcfg_add("milter", DEFAULT_SOCKET);
	}
}	

static void
provide_default_callout_server(void)
{
	struct variable *var;

	if (provide_callout &&
	    !callout_server_url &&
	    (!(var = variable_lookup("callout_server_url")) ||
	     (var->sym.flags & SYM_REFERENCED) &&
	     !(var->sym.flags & SYM_INITIALIZED))) {
		mf_srvcfg_add("callout", DEFAULT_CALLOUT_SOCKET);
	}
}


static char *modnames[] = {
#define __DBGMOD_C_ARRAY
# include "mfd-dbgmod.h"
#undef __DBGMOD_C_ARRAY
	NULL
};

mu_debug_handle_t mfd_debug_handle;

static void
debug_init(void)
{
	int i;
	
	mfd_debug_handle = mu_debug_register_category (modnames[0]);
	for (i = 1; modnames[i]; i++)
		mu_debug_register_category (modnames[i]);
}


int
mf_server_function(const char *key, struct mf_srvcfg *cfg)
{
	if (!key || strcmp(key, "default") == 0 || strcmp(key, "milter") == 0)
		cfg->server = milter_session_server;
	else if (strcmp(key, "callout") == 0) {
		cfg->server = mfd_callout_session_server;
		if (cfg->defopt ||
		    mu_list_locate(cfg->options, "default", NULL) == 0)
			callout_server_url =
				mu_strdup(mu_url_to_string(cfg->url));
	} else
		return 1;
	return 0;
}

static void
open_strecho(int daemon_mode)
{
	int rc;
	if (daemon_mode) {
		rc = mu_stream_ioctl(mu_strerr, MU_IOCTL_LOGSTREAM,
				     MU_IOCTL_LOGSTREAM_CLONE,
				     &mf_strecho);
		if (rc == 0) {
			int s = MU_LOG_INFO;
			mu_stream_ioctl(mf_strecho, MU_IOCTL_LOGSTREAM,
					MU_IOCTL_LOGSTREAM_SET_SEVERITY,
					&s);
			/* Clear severity suppression, if any */
			s = MU_LOG_DEBUG;
			mu_stream_ioctl(mf_strecho, MU_IOCTL_LOGSTREAM,
					MU_IOCTL_LOGSTREAM_SUPPRESS_SEVERITY,
					&s);
		}
	} else if (echo_output) {
		if (strcmp(echo_output, "-") == 0) {
			mf_strecho = mu_strout;
			mu_stream_ref(mf_strecho);
			rc = 0;
		} else {
			rc = mu_file_stream_create(&mf_strecho, echo_output,
						   MU_STREAM_WRITE|MU_STREAM_CREAT);
		}
	} else {
		rc = mu_stdio_stream_create(&mf_strecho, MU_STDERR_FD, 0);
	}
	if (rc) {
		mu_diag_output(MU_LOG_CRIT,
			       _("cannot create echo output stream: %s"),
			       mu_strerror(rc));
		exit(EX_UNAVAILABLE);
	}
}
	
void
mailfromd_alloc_die()
{
	parse_error("not enough memory");
        abort();
}

extern char **environ;

struct mu_cli_setup cli = {
	.optv = options,
	.cfg = mf_cfg_param,
	.prog_doc = prog_doc,
	.prog_args = args_doc
};

/*
 * Split command line arguments into macro definitions, script file
 * name and proper arguments, depending on the current mode.
 *
 * On input, pargc points to the number of arguments in the command
 * line and pargv to the vector of arguments.  On output, both pargc
 * and pargv are updated.  The script_name is assigned the name of
 * the MFL script file (or NULL), mc and mv keep number of macro
 * definitions and vector of these.
 *
 * On error, prints a diagnostic message and exits with EX_USAGE
 * status.
 */
static void
argv_split(int *pargc, char ***pargv, char **script_name,
	   int *mc, char ***mv)
{
	int argc = *pargc;
	char **argv = *pargv;
	int i, n = -1;

	*script_name = NULL;
	*mc = 0;
	*mv = NULL;

	switch (mode) {
	case MAILFROMD_DAEMON:
		if (argc == 1) {
			*script_name = argv[0];
			argc--;
			argv++;
		} else if (argc > 1) {
			mu_error(_("extra arguments in daemon mode"));
			exit(EX_USAGE);
		}
		break;
		
	case MAILFROMD_TEST:
		for (i = 0; i < argc; i++) {
			if (strchr(argv[i], '=') == 0) {
				if (n == -1) {
					n = i;
				} else {
					mu_error(_("script file specified "
						   "twice (%s and %s)"),
						 argv[n], argv[i]);
					exit(EX_USAGE);
				}
			}
		}

		if (n >= 0) {
			*script_name = argv[n];
			memmove(argv + n, argv + n + 1,
				(argc - n + 1) * sizeof argv[0]);
			argc--;
		}

		*mc = argc;
		*mv = argv;
		argv += argc;
		argc = 0;
		break;

	case MAILFROMD_RUN:
		for (i = 0; i < argc; i++) {
			if (strchr(argv[i], '=') == 0) {
				n = i;
				break;
			}
		}

		if (n >= 0) {
			*script_name = argv[n];
			argv[n] = NULL;
			*mc = n;
			*mv = argv;

			argv += n + 1;
			argc -= n + 1;
		}
		break;

	default:
		mu_error(_("extra arguments in this mode"));
		exit(EX_USAGE);
	}

	*pargc = argc;
	*pargv = argv;
}

int
main(int argc, char **argv)
{
	prog_counter_t entry_point;
	int stderr_is_closed = stderr_closed_p();
	int macc;
	char **macv;
	
	mf_init_nls();
	mf_proctitle_init(argc, argv, environ);
	mu_alloc_die_hook = mailfromd_alloc_die;
	
	MU_AUTH_REGISTER_ALL_MODULES();
	mu_register_all_formats();
	mu_register_all_mailer_formats();
	yy_flex_debug = 0;

	/* Initialize milter */
	milter_setlogmask(SMI_LOG_FROM(SMI_LOG_WARN));
	milter_settimeout(7200);

	/* Set default logging */
	mu_set_program_name(argv[0]);
	mu_log_tag = mu_strdup(mu_program_name);
	mu_log_facility = DEFAULT_LOG_FACILITY;
	mu_log_print_severity = 1;
	mu_stdstream_setup(MU_STDSTREAM_RESET_NONE);
	mf_srvcfg_log_setup(stderr_is_closed ? "syslog" : "stderr");

	debug_init();
	libcallout_init();
	init_string_space();
	init_symbols();
	builtin_setup();
	mf_runtime_param_finish();
	db_format_setup();
	pragma_setup();
	
	mf_server_save_cmdline(argc, argv);

	dnsbase_init();
	mu_acl_cfg_init();
	database_cfg_init();
	srvman_init();
	mf_srvcfg_init(argv[0], "(milter | callout)");
	preprocessor_cfg_init();
	
	mf_getopt(&cli, &argc, &argv, capa, args_in_order(argc, argv));

	if (validate_options())
		exit(EX_USAGE);

	init_relayed_domains();
	if (resolv_conf_file)
		dnsbase_file_init(resolv_conf_file);
	
	if (trace_option != ARG_UNSET)
		do_trace = trace_option;
	if (trace_modules) {
		mu_list_foreach(trace_modules, flush_trace_module, NULL);
		mu_list_destroy(&trace_modules);
	}

	mf_srvcfg_flush();
	
	preprocessor_finalize();

	if (need_script) {
		argv_split(&argc, &argv, &script_file, &macc, &macv);
		if (script_file == NULL) {
			script_file = path_find_file(SYSCONFDIR,
						     DEFAULT_SCRIPT_FILE_STEM,
						     default_suffixes);
			if (!script_file) {
				mu_error(_("filter script file %s/%s%s does not exist or is unreadable"),
					 SYSCONFDIR, DEFAULT_SCRIPT_FILE_STEM,
					default_suffixes[0]);
				exit(EX_NOINPUT);
			}
		}

		if (script_file[0] != '/')
			/* Clear saved command line */
			mf_server_save_cmdline(0, NULL); 
		if (preprocess_option)
			exit(preprocess_input());
		if (parse_program(script_file, script_ydebug))
			exit(EX_CONFIG);
	}

	if (script_dump_tree) 
		print_syntax_tree();
	if (script_dump_code)
		print_code();

	if (script_dump_xref)
		print_xref();
	
	if (script_dump_macros)
		print_used_macros();
	
	fixup_code();
	
	if (script_check || script_dump_macros
	    || script_dump_code || script_dump_tree || script_dump_xref
	    || yy_flex_debug || script_ydebug)
		exit(EX_OK);

	switch (mode) {
	case MAILFROMD_DAEMON:
		mf_server_log_setup();
		provide_default_milter_server();
		provide_default_callout_server();
		break;

	case MAILFROMD_RUN: {
		struct function *fun = function_lookup(main_function_name);
		if (!fun) {
			mu_error(_("function %s is not defined"),
				 main_function_name);
			exit(EX_CONFIG);
		}
	
		if (fun->parmcount || !fun->varargs) {
			mu_error(_("function %s must take variable number of "
				   "arguments"),
				 main_function_name);
			exit(EX_CONFIG);
		}
		if (fun->rettype != dtype_number) {
			mu_error(_("function %s must return number"),
				 main_function_name);
			exit(EX_CONFIG);
		}
		entry_point = fun->entry;
	}
	}

	free_exceptions();
	free_symbols();
	free_string_space();
	free_parser_data();

	mf_namefixup_run(mailfromd_state_dir);
	mf_namefixup_free();
	
	switch (mode) {
	case MAILFROMD_DAEMON:
		if (script_file[0] != '/') {
			mu_diag_output(MU_DIAG_WARNING,
				       _("script file is given "
					 "without full file name"));
			server_flags |= MF_SERVER_NORESTART;
		}
		open_strecho(1);
		/* Startup handlers should be run after assuming user privs. */
		mf_server_startup_hook = run_startup;
		mf_server_shutdown_hook = run_shutdown;
		mf_server_lint_option = "--lint";
		mf_server_start("mailfromd", mailfromd_state_dir, pidfile,
				server_flags);
		break;
		
	case MAILFROMD_TEST:
		logger_set_print_severity(0);
		open_strecho(0);
		run_startup();
		mailfromd_test(macc, macv);
		run_shutdown();
		break;
		
	case MAILFROMD_SHOW_DEFAULTS:
		mailfromd_show_defaults();
		break;

	case MAILFROMD_RUN:
		logger_set_print_severity(0);
		open_strecho(0);
		run_startup();
		mailfromd_run(entry_point, argc, argv, macc, macv);
		run_shutdown();
	}

	exit(EX_OK);
}

