diff dccm/dccm.c @ 0:c7f6b056b673

First import of vendor version
author Peter Gervai <grin@grin.hu>
date Tue, 10 Mar 2009 13:49:58 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dccm/dccm.c	Tue Mar 10 13:49:58 2009 +0100
@@ -0,0 +1,1506 @@
+/* Distributed Checksum Clearinghouse
+ *
+ * sendmail milter interface
+ *
+ * Copyright (c) 2008 by Rhyolite Software, LLC
+ *
+ * This agreement is not applicable to any entity which sells anti-spam
+ * solutions to others or provides an anti-spam solution as part of a
+ * security solution sold to other entities, or to a private network
+ * which employs the DCC or uses data provided by operation of the DCC
+ * but does not provide corresponding data to other users.
+ *
+ * Permission to use, copy, modify, and distribute this software without
+ * changes for any purpose with or without fee is hereby granted, provided
+ * that the above copyright notice and this permission notice appear in all
+ * copies and any distributed versions or copies are either unchanged
+ * or not called anything similar to "DCC" or "Distributed Checksum
+ * Clearinghouse".
+ *
+ * Parties not eligible to receive a license under this agreement can
+ * obtain a commercial license to use DCC by contacting Rhyolite Software
+ * at sales@rhyolite.com.
+ *
+ * A commercial license would be for Distributed Checksum and Reputation
+ * Clearinghouse software.  That software includes additional features.  This
+ * free license for Distributed ChecksumClearinghouse Software does not in any
+ * way grant permision to use Distributed Checksum and Reputation Clearinghouse
+ * software
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE, LLC DISCLAIMS ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE, LLC
+ * BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
+ * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+ * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
+ *
+ * Rhyolite Software DCC 1.3.103-1.238 $Revision$
+ */
+
+#include "libmilter/mfapi.h"
+#include "cmn_defs.h"
+
+#undef NEW_MFAPI
+#ifdef SM_LM_VRS_MAJOR
+#if SM_LM_VRS_MAJOR(SMFI_VERSION) >= 1
+#define NEW_MFAPI
+#endif
+#endif
+
+u_char cannot_discard = 0;		/* can trim targets after DATA */
+u_char cannot_reject = 0;
+
+static u_char background = 1;
+static DCC_PATH pidpath;
+
+static const char *progpath = DCC_LIBEXECDIR"/dccm";
+
+static DCC_PATH conn_def;
+static char *milter_conn = conn_def;	/* MILTER socket specification */
+
+static char sm_isspam_macro_def[] = "{dcc_isspam}";
+static char *sm_isspam_macro = sm_isspam_macro_def;
+static char sm_notspam_macro_def[] = "{dcc_notspam}";
+static char *sm_notspam_macro = sm_notspam_macro_def;
+
+/* DCC-milter state or context */
+typedef struct work {
+    SMFICTX	*milter_ctx;
+#    define	 WORK_MILTER_CTX_IDLE ((SMFICTX *)DCC_SRVR_PORT)
+    CMN_WORK	cw;
+#    define	 NUM_XHDRS 5
+    struct {				/* existing X-DCC headers */
+	u_char	    num;
+	u_char	    len;
+	char	    brand[DCC_BRAND_MAXLEN];
+    } xhdrs[NUM_XHDRS];
+    REPLY_TPLT	sendmail_reply;
+    /* from here down is zeroed when the structure is allocated */
+#define WORK_ZERO fwd
+    struct work *fwd;
+    /* from here down is zeroed when the structure is used for a 2nd msg */
+#define WORK_REZERO num_x_dcc
+    u_char	num_x_dcc;
+} WORK;
+
+#define WORK_EXCESS ((WORK *)1)
+
+
+/* use a free list to avoid malloc() overhead */
+static WORK *work_free;
+static int work_too_many;
+static time_t work_msg_time;
+
+/* each dccm job involves
+ *	a socket connected to sendmail,
+ *	a log file,
+ *	and a socket to talk to the DCC server.
+ * The file descriptors for the whitelists are accounted for in EXTRA_FILES */
+#define FILES_PER_JOB	3
+int max_max_work = MAX_SELECT_WORK;
+
+
+static sfsistat dccm_conn(SMFICTX *, char *, _SOCK_ADDR *);
+static sfsistat dccm_helo(SMFICTX *, char *);
+static sfsistat dccm_envfrom(SMFICTX *, char **);
+static sfsistat dccm_envrcpt(SMFICTX *, char **);
+static sfsistat dccm_header(SMFICTX *, char *, char *);
+static sfsistat dccm_eoh(SMFICTX *);
+static sfsistat dccm_body(SMFICTX *, u_char *, size_t);
+static sfsistat dccm_eom(SMFICTX *);
+static sfsistat dccm_abort(SMFICTX *);
+static sfsistat dccm_close(SMFICTX *);
+#ifdef NEW_MFAPI
+static sfsistat dccm_negotiate(SMFICTX *, unsigned long, unsigned long,
+			       unsigned long, unsigned long,
+			       unsigned long *, unsigned long *,
+			       unsigned long *, unsigned long *);
+#endif
+
+static char dccm_name[] = {"DCC"};
+static struct smfiDesc smfilter = {
+    dccm_name,				/* filter name */
+    SMFI_VERSION,			/* version code -- do not change */
+    SMFIF_CHGHDRS | SMFIF_ADDHDRS | SMFIF_DELRCPT,  /* flags */
+    dccm_conn,				/* connection info filter */
+    dccm_helo,				/* SMTP HELO command filter */
+    dccm_envfrom,			/* envelope sender filter */
+    dccm_envrcpt,			/* envelope recipient filter */
+    dccm_header,			/* header filter */
+    dccm_eoh,				/* end of header */
+    dccm_body,				/* body block filter */
+    dccm_eom,				/* end of message */
+    dccm_abort,				/* message aborted */
+    dccm_close,				/* connection finished */
+#ifdef NEW_MFAPI
+    0,					/* unknown SMTP command */
+    0,					/* xxfi_data */
+    dccm_negotiate,			/* negotiate new milter options */
+#endif
+};
+
+
+static REPLY_TPLT too_many_reply = {
+	DCC_XHDR_TOO_MANY_RCPTS, {REPLY_TPLT_NULL},
+	"452", "4.5.3", 0, DCC_XHDR_TOO_MANY_RCPTS};
+
+static REPLY_TPLT incompat_white_reply = {
+	DCC_XHDR_INCOMPAT_WLIST, {REPLY_TPLT_NULL},
+	"452", "4.5.3", 0, DCC_XHDR_INCOMPAT_WLIST};
+
+
+static char *add_braces(const char *);
+static void del_sock(void);
+static void add_work(int);
+
+
+static void
+usage(const char* barg, const char *bvar)
+{
+	const char str[] = {
+	    "usage: [-VdbxANQ] [-G on | off | noIP | IPmask/xx] [-h homedir]"
+	    " [-I user]\n"
+	    "    [-p protocol:filename | protocol:port@host] [-m map]\n"
+	    "    [-w whiteclnt] [-U userdirs] [-a IGNORE | REJECT | DISCARD]\n"
+	    "    [-t type,[log-thold,][spam-thold]]"
+	    " [-g [not-]type] [-S header]\n"
+	    "    [-l logdir] [-R rundir] [-r rejection-msg] [-j maxjobs]\n"
+	    "    [-B dnsbl-option] [-L ltype,facility.level]"
+	};
+	static u_char complained;
+
+	if (!complained) {
+		if (barg)
+			dcc_error_msg("unrecognized \"%s%s\"\n%s\n..."
+				      " continuing",
+				      barg, bvar, str);
+		else
+			dcc_error_msg("%s\n... continuing", str);
+		complained = 1;
+	}
+}
+
+
+int NRATTRIB
+main(int argc, char **argv)
+{
+	DCC_EMSG emsg;
+#ifdef RLIMIT_NOFILE
+	struct rlimit nofile;
+	int old_rlim_cur;
+#endif
+	long l;
+	u_char log_tgts_set = 0;
+	time_t smfi_main_start;
+	char *p;
+	const char *rundir = DCC_RUNDIR;
+	const char *homedir = 0;
+	const char *logdir = 0;
+	int result, i;
+
+	emsg[0] = '\0';
+	if (*argv[0] == '/')
+		progpath = argv[0];
+	dcc_syslog_init(1, argv[0], 0);
+	dcc_clear_tholds();
+
+#ifdef RLIMIT_NOFILE
+	if (0 > getrlimit(RLIMIT_NOFILE, &nofile)) {
+		dcc_error_msg("getrlimit(RLIMIT_NOFILE): %s", ERROR_STR());
+		old_rlim_cur = 1000*1000;
+	} else {
+		old_rlim_cur = nofile.rlim_cur;
+		if (nofile.rlim_max < 1000*1000) {
+			i = nofile.rlim_max;
+#ifndef USE_POLL
+			if (i > FD_SETSIZE)
+				i = FD_SETSIZE;
+#endif
+			max_max_work = (i - EXTRA_FILES)/FILES_PER_JOB;
+			max_max_work_src = "RLIMIT_NOFILE limit";
+		}
+	}
+#endif /* RLIMIT_NOFILE */
+	if (max_max_work <= 0) {
+		dcc_error_msg("too few open files allowed");
+		max_max_work = MIN_MAX_WORK;
+	}
+	max_work = max_max_work;
+
+#define SLARGS "VdbxANQW"		/* change start-dccm if these change */
+	while (EOF != (i = getopt(argc, argv, SLARGS"G:h:I:"
+				  "p:m:w:U:a:t:g:S:l:R:r:s:o:j:B:L:"))) {
+		switch (i) {
+		case 'V':
+			fprintf(stderr, DCC_VERSION"\n");
+			exit(EX_OK);
+			break;
+
+		case 'd':
+			++dcc_clnt_debug;
+			break;
+
+		case 'b':
+			background = 0;
+			break;
+
+		case 'x':
+			try_extra_hard = DCC_CLNT_FG_NO_FAIL;
+			break;
+
+		case 'A':
+			chghdr = ADDHDR;
+			smfilter.xxfi_flags &= ~SMFIF_CHGHDRS;
+			smfilter.xxfi_flags |= SMFIF_ADDHDRS;
+			break;
+
+		case 'N':
+			chghdr = NOHDR;
+			smfilter.xxfi_flags &= ~(SMFIF_ADDHDRS | SMFIF_CHGHDRS);
+			break;
+
+		case 'Q':
+			dcc_query_only = 1;
+			break;
+
+		case 'W':		/* obsolete DCC off by default */
+			to_white_only = 1;
+			break;
+
+		case 'G':
+			if (!dcc_parse_client_grey(optarg))
+				usage("-G", optarg);
+			break;
+
+		case 'h':
+			homedir = optarg;
+			break;
+
+		case 'I':
+			dcc_daemon_su(optarg);
+			break;
+
+		case 'p':
+			milter_conn = optarg;
+			break;
+
+		case 'm':
+			mapfile_nm = optarg;
+			break;
+
+		case 'w':
+			main_white_nm = optarg;
+			break;
+
+		case 'U':
+			parse_userdirs(optarg);
+			break;
+
+		case 'a':
+			if (!strcasecmp(optarg, "IGNORE")) {
+				action = CMN_IGNORE;
+			} else if (!strcasecmp(optarg, "REJECT")) {
+				action = CMN_REJECT;
+			} else if (!strcasecmp(optarg, "DISCARD")) {
+				action = CMN_DISCARD;
+			} else {
+				dcc_error_msg("unrecognized -a action: %s",
+					      optarg);
+			}
+			break;
+
+		case 't':
+			if (dcc_parse_tholds("-t ", optarg))
+				log_tgts_set = 1;
+			break;
+
+		case 'g':		/* honor not-spam "counts" */
+			dcc_parse_honor(optarg);
+			break;
+
+		case 'S':
+			dcc_add_sub_hdr(0, optarg);
+			break;
+
+		case 'l':		/* log rejected mail here */
+			logdir = optarg;
+			break;
+
+		case 'R':
+			rundir = optarg;
+			break;
+
+		case 'r':
+			parse_reply_arg(optarg);
+			break;
+
+		case 's':		/* deprecated: set dcc_isspam */
+			sm_isspam_macro = add_braces(optarg);
+			break;
+
+		case 'o':		/* deprecated: set dcc_notspam */
+			sm_notspam_macro = add_braces(optarg);
+			break;
+
+		case 'j':		/* maximum simultaneous jobs */
+			l = strtoul(optarg, &p, 10);
+			if (*p != '\0' || l < MIN_MAX_WORK) {
+				dcc_error_msg("invalid queue length %s",
+					      optarg);
+			} else if (l > max_max_work) {
+				dcc_error_msg("-j queue length %s"
+					      " larger than %s; using %d",
+					      optarg,
+					      max_max_work_src, max_max_work);
+				max_work = max_max_work;
+			} else {
+				max_work = l;
+			}
+			break;
+
+		case 'B':
+			if (!dcc_parse_dnsbl(emsg, optarg, progpath, 0))
+				dcc_error_msg("%s", emsg);
+			break;
+
+		case 'L':
+			if (dcc_parse_log_opt(optarg))
+				helper_save_arg("-L", optarg);
+			break;
+
+		default:
+			usage(optopt2str(optopt), "");
+		}
+	}
+	if (argc != optind)
+		usage(argv[optind], "");
+
+	snprintf(conn_def, sizeof(conn_def), "%s/%s", rundir, dcc_progname);
+
+	dcc_cdhome(emsg, homedir, 0);
+	dcc_main_logdir_init(0, logdir);
+	if (dcc_main_logdir[0] == '\0') {
+		/* if not logging,
+		 * tell sendmail to not bother with some stuff */
+		smfilter.xxfi_helo = 0;
+
+		if (log_tgts_set)
+			dcc_error_msg("log thresholds set with -t"
+				      " but no -l directory");
+		if (userdirs != '\0')
+			dcc_error_msg("no -l directory prevents per-user"
+				      " logging with -U");
+	}
+
+
+#ifdef RLIMIT_NOFILE
+	i = max_work*FILES_PER_JOB+EXTRA_FILES;
+	if (old_rlim_cur < i) {
+		nofile.rlim_cur = i;
+		if (0 > setrlimit(RLIMIT_NOFILE, &nofile)) {
+			dcc_error_msg("setrlimit(RLIMIT_NOFILE,%d): %s",
+				      i, ERROR_STR());
+			max_work = old_rlim_cur/FILES_PER_JOB - EXTRA_FILES;
+			if (max_work <= 0) {
+				dcc_error_msg("only %d open files allowed"
+					      " by RLIMIT_NOFILE",
+					      old_rlim_cur);
+				max_work = MIN_MAX_WORK;
+			}
+		}
+	}
+#endif /* RLIMIT_NOFILE */
+
+	helper_init(max_work);
+
+	if (MI_SUCCESS != smfi_setconn(milter_conn))
+		dcc_logbad(EX_USAGE, "illegal sendmail connection"
+			   " \"%s\"\n", optarg);
+
+	del_sock();
+
+	if (smfi_register(smfilter) == MI_FAILURE)
+		dcc_logbad(EX_UNAVAILABLE, "smfi_register failed\n");
+
+	if (background) {
+		if (daemon(1, 0) < 0)
+			dcc_logbad(EX_OSERR, "daemon(): %s", ERROR_STR());
+
+		dcc_daemon_restart(rundir, del_sock);
+		dcc_pidfile(pidpath, rundir);
+	}
+	/* Be careful to start all threads only after the fork() in daemon(),
+	 * because some POSIX threads packages (e.g. FreeBSD) get confused
+	 * about threads in the parent.  */
+
+	cmn_init();
+	add_work(init_work);
+
+	dcc_trace_msg(DCC_VERSION" listening to %s with %s",
+		      milter_conn, dcc_homedir);
+	if (dcc_clnt_debug)
+		dcc_trace_msg("init_work=%d max_work=%d max_max_work=%d (%s)",
+			      total_work, max_work, max_max_work,
+			      max_max_work_src);
+
+	/* It would be nice to remove the UNIX domain socket and PID file
+	 * when smfi_main() returns, but we dare not because the library
+	 * delays for several seconds after being signalled to stop.
+	 * Our files might have been unlinked and the files now in
+	 * the filesystem might belong to some other process. */
+	smfi_main_start = time(0);
+	result = smfi_main();
+
+	if (pidpath[0] != '\0')
+		unlink(pidpath);
+
+	totals_stop();
+
+	/* The sendmail libmilter machinery sometimes gets confused and
+	 * gives up.  Try to start over if we had been running for at least
+	 * 10 minutes */
+	if (result != MI_SUCCESS
+	    && time(0) > smfi_main_start+10*60) {
+		dcc_error_msg("try to restart after smfi_main() = %d", result);
+		exit(EX_DCC_RESTART);
+	}
+
+	if (result != MI_SUCCESS)
+		dcc_error_msg("smfi_main() = %d", result);
+	exit((result == MI_SUCCESS) ? EX_OK : EX_UNAVAILABLE);
+}
+
+
+
+static char *
+add_braces(const char *s)
+{
+	int i;
+	char *new;
+
+	i = strlen(s);
+	if (i >= 2 && s[0] == '{' && s[i-1] == '}')
+		return strdup(s);
+	new = dcc_malloc(i+3);
+	new[0] = '{';
+	memcpy(new+1, s, i);
+	new[i+1] = '}';
+	new[i+2] = '\0';
+	return new;
+}
+
+
+
+/* remove the Unix domain socket of a previous instance of this daemon */
+static void
+del_sock(void)
+{
+	int s;
+	struct stat sb;
+	const char *conn;
+	struct sockaddr_un conn_sun;
+	int len, i;
+
+	/* Ignore the sendmail milter "local|whatever:" prefix.
+	 * If it is a UNIX domain socket, fine.  If not, no harm is done */
+	conn = strchr(milter_conn, ':');
+	if (conn)
+		++conn;
+	else
+		conn = milter_conn;
+
+	len = strlen(conn);
+	if (len >= ISZ(conn_sun.sun_path))
+		return;			/* perhaps not a UNIX domain socket */
+
+	memset(&conn_sun, 0, sizeof(conn_sun));
+	conn_sun.sun_family = AF_LOCAL;
+	strcpy(conn_sun.sun_path, conn);
+#ifdef HAVE_SA_LEN
+	conn_sun.sun_len = SUN_LEN(&conn_sun);
+#endif
+
+	if (0 > stat(conn_sun.sun_path, &sb))
+		return;
+	if (!(S_ISSOCK(sb.st_mode) || S_ISFIFO(sb.st_mode)))
+		dcc_logbad(EX_UNAVAILABLE, "non-socket present at %s",
+			   conn_sun.sun_path);
+
+	/* The sendmail libmilter seems to delay as long as 5 seconds
+	 * before stopping.  It delays indefinitely if an SMTP client
+	 * is stuck. */
+	i = 0;
+	for (;;) {
+		s = socket(AF_UNIX, SOCK_STREAM, 0);
+		if (s < 0) {
+			dcc_logbad(EX_OSERR, "socket(AF_UNIX): %s",
+				   ERROR_STR());
+			return;
+		}
+		if (++i > 5*10)
+			dcc_logbad(EX_UNAVAILABLE,
+				   "DCCM or something already or still running"
+				   " with socket at %s",
+				   conn_sun.sun_path);
+		if (0 > connect(s, (struct sockaddr *)&conn_sun,
+				sizeof(conn_sun))) {
+			/* unlink it only if it looks like a dead socket */
+			if (errno == ECONNREFUSED || errno == ECONNRESET
+			    || errno == EACCES) {
+				if (0 > unlink(conn_sun.sun_path))
+					dcc_error_msg("unlink(old %s): %s",
+						      conn_sun.sun_path,
+						      ERROR_STR());
+			} else {
+				dcc_error_msg("connect(old %s): %s",
+					      conn_sun.sun_path, ERROR_STR());
+			}
+			close(s);
+			break;
+		}
+		close(s);
+		usleep(100*1000);
+	}
+}
+
+
+
+/* create some contexts. */
+static void
+add_work(int i)
+{
+	WORK *wp;
+
+	total_work += i;
+
+	wp = dcc_malloc(sizeof(*wp)*i);
+	memset(wp, 0, sizeof(*wp)*i);
+
+	while (i-- != 0) {
+		wp->milter_ctx = WORK_MILTER_CTX_IDLE;
+		cmn_create(&wp->cw);
+		wp->fwd = work_free;
+		work_free = wp;
+		++wp;
+	}
+}
+
+
+
+static WORK *
+work_alloc(void)
+{
+	WORK *wp;
+
+	lock_work();
+	wp = work_free;
+	if (!wp) {
+		if (total_work > max_work) {
+			++work_too_many;
+			unlock_work();
+			return 0;
+		}
+		if (dcc_clnt_debug > 1)
+			dcc_trace_msg("add %d work blocks to %d",
+				      init_work, total_work);
+		add_work(init_work);
+		wp = work_free;
+	}
+	if (wp->milter_ctx != WORK_MILTER_CTX_IDLE)
+		dcc_logbad(EX_SOFTWARE, "corrupt WORK area");
+	work_free = wp->fwd;
+	unlock_work();
+
+	/* clear most of it */
+	cmn_clear(&wp->cw, wp, 1);
+	wp->cw.helo[0] = '\0';
+	memset(&wp->WORK_ZERO, 0,
+	       sizeof(*wp) - ((char*)&wp->WORK_ZERO - (char*)wp));
+
+	return wp;
+}
+
+
+
+/* ocassionally close sockets to recover from dictionary attacks */
+void
+work_clean(void)
+{
+	WORK *wp;
+	int keep, delete;
+
+	lock_work();
+	keep = 5;
+	delete = init_work;
+	for (wp = work_free; wp; wp = wp->fwd) {
+		if (!wp->cw.dcc_ctxt)
+			break;
+		if (--keep > 0)
+			continue;
+		dcc_clnt_soc_close(wp->cw.dcc_ctxt);
+		if (--delete <= 0)
+			break;
+	}
+	unlock_work();
+}
+
+
+
+typedef enum {GET_WP_START,		/* not yet seen dccm_envfrom() */
+	GET_WP_GOING,			/* have seen dccm_envfrom() */
+	GET_WP_ABORT,			/* dccm_abort() */
+	GET_WP_CLOSE			/* dccm_close() */
+} GET_WP_MODE;
+static WORK *
+get_wp(SMFICTX *milter_ctx,
+       GET_WP_MODE mode)
+{
+	WORK *wp;
+
+	wp = (WORK *)smfi_getpriv(milter_ctx);
+	if (!wp) {
+		/* milter context is not active */
+		if (mode == GET_WP_CLOSE || mode == GET_WP_ABORT)
+			return 0;
+		dcc_logbad(EX_SOFTWARE, "null SMFICTX pointer");
+	} else if (wp == WORK_EXCESS) {
+		if (mode == GET_WP_START || mode == GET_WP_GOING)
+			dcc_logbad(EX_SOFTWARE, "tardy WORK_EXCESS");
+		if (dcc_clnt_debug)
+			dcc_trace_msg("%s for excessive message",
+				      mode == GET_WP_ABORT
+				      ? "abort" : "close");
+		return 0;
+	}
+	if (wp->milter_ctx != milter_ctx)
+		dcc_logbad(EX_SOFTWARE,
+			   "bogus SMFICTX pointer or corrupt WORK area");
+
+	if (!wp->cw.dcc_ctxt && (mode == GET_WP_START || mode == GET_WP_GOING))
+		dcc_logbad(EX_SOFTWARE, "tardy failure to find ctxt");
+
+	if (wp->cw.env_from[0] == '\0' && mode == GET_WP_GOING)
+		dcc_logbad(EX_SOFTWARE, "work cleared?");
+
+	return wp;
+}
+
+
+
+static void
+set_sendmail_reply(WORK *wp,
+		   const char *rcode, const char *xcode, const char *str)
+{
+	int i;
+
+	/* kludge to fix lack of const declaration */
+	typedef int (*SR)(SMFICTX *, const char *, const char *, const char *);
+	static SR sr = (SR)smfi_setreply;
+	i = (*sr)(wp->milter_ctx, rcode, xcode, str);
+
+	if (i != MI_SUCCESS)
+		thr_error_msg(&wp->cw, "smfi_setreply(\"%s\",\"%s\",\"%s\")=%d",
+			      rcode, xcode, str, i);
+}
+
+
+
+/* refuse one recipient */
+static sfsistat
+rcpt_tempfail(WORK *wp, RCPT_ST *rcpt_st, const REPLY_TPLT *tplt)
+{
+	REPLY_STRS strs;
+
+	make_reply(&strs, tplt, &wp->cw, 0);
+	set_sendmail_reply(wp, strs.rcode, strs.xcode, strs.str);
+	wp->cw.ask_st |= ASK_ST_LOGIT;
+	if (rcpt_st) {
+		snprintf(rcpt_st->rej_msg, sizeof(rcpt_st->rej_msg),
+			 "%s %s %s", strs.rcode, strs.xcode, strs.str);
+		rcpt_st->rej_result = strs.log_result;
+		rcpt_st->fgs |= RCPT_FG_REJ_FILTER;
+	}
+	return SMFIS_TEMPFAIL;
+}
+
+
+
+static void
+msg_clear(WORK *wp)
+{
+	cmn_clear(&wp->cw, wp, 0);
+	memset(&wp->WORK_REZERO, 0,
+	       sizeof(*wp) - ((char*)&wp->WORK_REZERO - (char*)wp));
+}
+
+
+
+/* we are finished with one SMTP message.
+ * get ready for the next from the same connection to an SMTP client */
+static void
+msg_done(WORK *wp, const char *result)
+{
+	LOG_CAPTION(wp, DCC_XHDR_RESULT);
+	log_write(&wp->cw, result ? result : DCC_XHDR_RESULT_ACCEPT, 0);
+	LOG_EOL(wp);
+
+	msg_clear(wp);
+}
+
+
+
+/* give up on entire message */
+static sfsistat
+msg_tempfail(WORK *wp, const REPLY_TPLT *tplt)
+{
+	make_reply(&wp->cw.reply, tplt, &wp->cw, 0);
+	set_sendmail_reply(wp, wp->cw.reply.rcode, wp->cw.reply.xcode,
+			   wp->cw.reply.str);
+	log_smtp_reply(&wp->cw);
+	wp->cw.ask_st |= ASK_ST_LOGIT;
+	msg_done(wp, wp->cw.reply.log_result);
+	return SMFIS_TEMPFAIL;
+}
+
+
+
+static sfsistat
+msg_reject(WORK *wp)
+{
+	sfsistat result;
+
+	/* temporize if we have not figured out what to say */
+	if (!wp->cw.reply.log_result) {
+		thr_error_msg(&wp->cw, "rejection reason undecided");
+		make_reply(&wp->cw.reply, &dcc_fail_reply, &wp->cw, 0);
+	}
+
+	set_sendmail_reply(wp, wp->cw.reply.rcode, wp->cw.reply.xcode,
+			   wp->cw.reply.str);
+	log_smtp_reply(&wp->cw);
+
+	result = (wp->cw.reply.rcode[0] == '4') ? SMFIS_TEMPFAIL : SMFIS_REJECT;
+	msg_done(wp, wp->cw.reply.log_result);
+	return result;
+}
+
+
+
+/* see what sendmail had to say about the message */
+static void
+ask_sm(SMFICTX *milter_ctx, WORK *wp)
+{
+	const char *m;
+
+	/* Do this only until we get an answer.
+	 * The sendmail macro might not be set on the first rcpt_to command.
+	 * If the is-spam macro is set before the not-spam macro, then this
+	 * will get the wrong answer.  However, undoing the effects of an
+	 * is-spam setting would be a mess, because they included turning
+	 * off DNSBL checks.  */
+	if ((wp->cw.ask_st & (ASK_ST_MTA_NOTSPAM | ASK_ST_MTA_ISSPAM)) != 0)
+		return;
+
+	if (0 != (m = smfi_getsymval(milter_ctx, sm_notspam_macro))
+	    && *m != '\0') {
+		/* We have a sendmail macro name that indicates a
+		 * whitelisting from sendmail rules and databases,
+		 * and the macro is set. */
+		wp->cw.ask_st |= ASK_ST_MTA_NOTSPAM;
+		wp->cw.ask_st &= ~ASK_ST_MTA_ISSPAM;
+		thr_log_print(&wp->cw, 1,
+			      "sendmail.cf"DCC_XHDR_ISOK": \"%s\"\n", m);
+
+	} else if (!(wp->cw.ask_st & ASK_ST_MTA_ISSPAM)
+		   && 0 != (m = smfi_getsymval(milter_ctx, sm_isspam_macro))
+		   && *m != '\0') {
+		wp->cw.ask_st |= ASK_ST_MTA_ISSPAM;
+
+		make_tplt(&wp->sendmail_reply, 0, DCC_XCODE, DCC_RCODE, m,
+			  DCC_XHDR_RESULT_REJECT);
+
+		thr_log_print(&wp->cw, 1, "sendmail.cf-->%s: \"%s\"\n",
+			      sm_isspam_macro, wp->sendmail_reply.pat);
+
+		make_reply(&wp->cw.reply, &wp->sendmail_reply, &wp->cw, 0);
+
+		if (!CLITCMP(wp->cw.reply.str, "DISCARD")) {
+			wp->cw.reply.str += LITZ("DISCARD");
+			wp->cw.reply.str += strspn(wp->cw.reply.str,
+						   DCC_WHITESPACE":");
+			wp->cw.action = CMN_DISCARD;
+		} else {
+			wp->cw.action = CMN_REJECT;
+		}
+	}
+}
+
+
+
+void
+user_reject_discard(CMN_WORK *cwp, RCPT_ST *rcpt_st)
+{
+	int i;
+
+	/* one of the other targets wants this message,
+	 * try to remove this address from sendmail's list */
+	i = smfi_delrcpt(cwp->wp->milter_ctx, rcpt_st->env_to);
+	if (MI_SUCCESS != i)
+		thr_error_msg(cwp, "delrcpt(%s)=%d", rcpt_st->env_to, i);
+}
+
+
+
+#ifdef NEW_MFAPI
+/* ask sendmail to tell us about rejected recipients */
+static sfsistat
+dccm_negotiate(SMFICTX *milter_ctx UATTRIB,
+	       unsigned long f0, unsigned long f1,
+	       unsigned long f2 UATTRIB, unsigned long f3 UATTRIB,
+	       unsigned long *pf0, unsigned long *pf1 UATTRIB,
+	       unsigned long *pf2 UATTRIB, unsigned long *pf3 UATTRIB)
+{
+	*pf0 = f0;
+	*pf1 = SMFIP_RCPT_REJ & f1;
+
+	return SMFIS_CONTINUE;
+}
+#endif /* NEW_MFAPI */
+
+
+
+/* start a new connection to an SMTP client */
+static sfsistat
+dccm_conn(SMFICTX *milter_ctx,
+	  char *name,			/* SMTP client hostname */
+	  _SOCK_ADDR *sender)
+{
+	WORK *wp;
+
+	wp = (WORK *)smfi_getpriv(milter_ctx);
+	if (wp) {
+		dcc_error_msg("bogus initial SMFICTX pointer");
+		smfi_setpriv(milter_ctx, 0);
+		return SMFIS_TEMPFAIL;
+	}
+	wp = work_alloc();
+	if (!wp) {
+		smfi_setpriv(milter_ctx, WORK_EXCESS);
+		return SMFIS_TEMPFAIL;
+	}
+	smfi_setpriv(milter_ctx, wp);
+	wp->milter_ctx = milter_ctx;
+
+	log_start(&wp->cw);
+
+	if (!name) {
+		if (dcc_clnt_debug)
+			thr_trace_msg(&wp->cw, "null sender name");
+		strcpy(wp->cw.clnt_name, "(null name)");
+	} else {
+		BUFCPY(wp->cw.clnt_name, name);
+	}
+
+	if (!sender) {
+		if (!strcasecmp(wp->cw.clnt_name, "localhost")) {
+			wp->cw.clnt_addr.s6_addr32[3] = htonl(0x7f000001);
+			wp->cw.clnt_addr.s6_addr32[0] = 0;
+			wp->cw.clnt_addr.s6_addr32[1] = 0;
+			wp->cw.clnt_addr.s6_addr32[2] = htonl(0xffff);
+			strcpy(wp->cw.clnt_str, "127.0.0.1");
+		} else {
+			if (dcc_clnt_debug)
+				thr_trace_msg(&wp->cw,
+					      "null sender address for \"%s\"",
+					      wp->cw.clnt_name);
+			wp->cw.clnt_str[0] = '\0';
+		}
+	} else if (sender->sa_family != AF_INET
+		   && sender->sa_family != AF_INET6) {
+		dcc_error_msg("unexpected sender address family %d",
+			      sender->sa_family);
+		wp->cw.clnt_str[0] = '\0';
+	} else {
+		if (sender->sa_family == AF_INET) {
+			dcc_ipv4toipv6(&wp->cw.clnt_addr,
+				       ((struct sockaddr_in*)sender)->sin_addr);
+			dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str),
+				      &wp->cw.clnt_addr);
+		} else if (sender->sa_family == AF_INET6) {
+			memcpy(&wp->cw.clnt_addr,
+			       &((struct sockaddr_in6 *)sender)->sin6_addr,
+			       sizeof(wp->cw.clnt_addr));
+			dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str),
+				      &wp->cw.clnt_addr);
+		} else {
+			dcc_error_msg("unknown address family for \"%s\"",
+				      wp->cw.clnt_name);
+			wp->cw.clnt_str[0] = '\0';
+		}
+	}
+
+	/* quit now if we cannot find a free client context */
+	if (!ck_dcc_ctxt(&wp->cw))
+		return msg_tempfail(wp, &dcc_fail_reply);
+
+	/* This much is common for all of the messages that might
+	 * arrive through this connection to the SMTP client */
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+/* log HELO */
+static sfsistat
+dccm_helo(SMFICTX *milter_ctx, char *helo)
+{
+	WORK *wp;
+	int i;
+
+	wp = get_wp(milter_ctx, GET_WP_START);
+
+	i = strlen(helo);
+	if (i < ISZ(wp->cw.helo)) {
+		memcpy(wp->cw.helo, helo, i+1);
+	} else {
+		memcpy(wp->cw.helo, helo, ISZ(wp->cw.helo)-ISZ(DCC_HELO_CONT));
+		strcpy(&wp->cw.helo[ISZ(wp->cw.helo)-ISZ(DCC_HELO_CONT)],
+		       DCC_HELO_CONT);
+	}
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+/* deal with Mail From envelope value */
+static sfsistat
+dccm_envfrom(SMFICTX *milter_ctx, char **from)
+{
+	static char dollar_i[] = "i";
+	static char mail_host_macro[] = "{mail_host}";
+	static char dcc_mail_host_macro[] = "{dcc_mail_host}";
+	const char *id, *mail_host;
+	WORK *wp;
+
+	wp = get_wp(milter_ctx, GET_WP_START);
+
+	log_start(&wp->cw);
+
+	dcc_cks_init(&wp->cw.cks);
+	dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, &wp->cw, wp->cw.id);
+
+	/* Assume for now (and again if this is not the first transaction
+	 * for this SMTP session) that the sender is the current SMTP client
+	 * whiteclnt.  Received: headers might have the real sender */
+	strcpy(wp->cw.sender_name, wp->cw.clnt_name);
+	strcpy(wp->cw.sender_str, wp->cw.clnt_str);
+
+	/* see if the SMTP client is one of our MX forwarders */
+	if (wp->cw.sender_str[0] != '\0') {
+		/* we need the IP checksum in the usual place to look in
+		 * the whitelist for it */
+		dcc_get_ipv6_ck(&wp->cw.cks, &wp->cw.clnt_addr);
+		check_mx_listing(&wp->cw);
+	}
+
+	/* replace the message ID generated when the log file was started
+	 * with the sendmail message ID */
+	id = smfi_getsymval(milter_ctx, dollar_i);
+	if (id)
+		BUFCPY(wp->cw.id, id);
+
+	BUFCPY(wp->cw.env_from, from[0]);
+
+	/* Even if sendmail.cf sets the ${dcc_mail_host} macro,
+	 * FEATURE(delay_checks) can delay its setting until after
+	 * the MAIL command has been processed and this milter function
+	 * has been called. */
+	mail_host = smfi_getsymval(milter_ctx, dcc_mail_host_macro);
+	if (!mail_host || !*mail_host)
+		mail_host = smfi_getsymval(milter_ctx, mail_host_macro);
+	if (mail_host)
+		BUFCPY(wp->cw.mail_host, mail_host);
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+/* note another recipient */
+static sfsistat
+dccm_envrcpt(SMFICTX *milter_ctx, char **rcpt)
+{
+	static char rcpt_mailer[] = "{rcpt_mailer}";
+	static char rcpt_addr[] = "{rcpt_addr}";
+	static char dcc_userdir[] = "{dcc_userdir}";
+	const char *mailer, *addr, *dir;
+	WORK *wp;
+	RCPT_ST *rcpt_st;
+
+	wp = get_wp(milter_ctx, GET_WP_GOING);
+
+	rcpt_st = alloc_rcpt_st(&wp->cw, 1);
+	if (!rcpt_st)
+		return rcpt_tempfail(wp, 0, &too_many_reply);
+
+	BUFCPY(rcpt_st->env_to, rcpt[0]);
+
+	addr = smfi_getsymval(milter_ctx, rcpt_addr);
+	mailer = smfi_getsymval(milter_ctx, rcpt_mailer);
+
+#ifdef NEW_MFAPI
+	/* count rejected recipient as if the message would have been
+	 * delivered to it */
+	if (mailer && !strcmp(mailer, "error")) {
+		rcpt_st->fgs |= RCPT_FG_BAD_USERNAME;
+		if (!addr || addr[0] != '4')
+			++wp->cw.mta_rej_tgts;
+		return SMFIS_CONTINUE;
+	}
+#endif
+
+	if (addr)
+		BUFCPY(rcpt_st->user, addr);
+
+	/* pick a per-user whitelist and log directory */
+	dir = smfi_getsymval(milter_ctx, dcc_userdir);
+	if (dir) {
+		if (!get_user_dir(rcpt_st, dir, strlen(dir), 0, 0))
+			thr_trace_msg(&wp->cw, "%s", wp->cw.emsg);
+	} else if (mailer && addr) {
+		if (!get_user_dir(rcpt_st, mailer, strlen(mailer),
+				  addr, strlen(addr)))
+			thr_trace_msg(&wp->cw, "%s", wp->cw.emsg);
+	}
+
+	/* sendmail might need to force discarding */
+	ask_sm(milter_ctx, wp);
+	if (!cmn_compat_whitelist(&wp->cw, rcpt_st))
+		return rcpt_tempfail(wp, rcpt_st, &incompat_white_reply);
+
+	++wp->cw.tgts;
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+static sfsistat
+dccm_header(SMFICTX *milter_ctx, char *headerf, char *headerv)
+{
+	WORK *wp;
+	int f_len, v_len;
+	const char *cp;
+	int i, j;
+
+	wp = get_wp(milter_ctx, GET_WP_GOING);
+
+	if (!(wp->cw.cmn_fgs & CMN_FG_ENV_LOGGED))
+		thr_log_envelope(&wp->cw, 1);
+
+	f_len = strlen(headerf);
+	v_len = strlen(headerv);
+	if (wp->cw.log_fd >= 0) {
+		log_body_write(&wp->cw, headerf, f_len);
+		log_body_write(&wp->cw, ": ", LITZ(": "));
+		log_body_write(&wp->cw, headerv, v_len);
+		log_body_write(&wp->cw, "\n", 1);
+	}
+
+	/* compute DCC checksums for favored headers */
+	if (!strcasecmp(headerf, DCC_XHDR_TYPE_FROM)) {
+		dcc_get_cks(&wp->cw.cks, DCC_CK_FROM, headerv, 1);
+		return SMFIS_CONTINUE;
+	}
+	if (!strcasecmp(headerf, DCC_XHDR_TYPE_MESSAGE_ID)) {
+		dcc_get_cks(&wp->cw.cks, DCC_CK_MESSAGE_ID, headerv, 1);
+		return SMFIS_CONTINUE;
+	}
+	if (!strcasecmp(headerf, DCC_XHDR_TYPE_RECEIVED)) {
+		dcc_get_cks(&wp->cw.cks, DCC_CK_RECEIVED, headerv, 1);
+
+		/* parse Received: headers if we do not have a
+		 * non-MX-whitelisted sender IP address
+		 * and sendmail gave us a valid address so that
+		 * there is a slot in the log file for an address.
+		 * Parsing a Received header offered by a spammer is
+		 * prevented by only parsing those added by MX-whitelisted
+		 * IP ddresses */
+		if (wp->cw.cks.sums[DCC_CK_IP].type == DCC_CK_INVALID
+		    && wp->cw.log_ip_pos != 0) {
+			const char *rh;
+			int old_eof;
+
+			rh = parse_received(headerv, &wp->cw.cks,
+					    0, 0,   /* already have HELO */
+					    wp->cw.sender_str,
+					    sizeof(wp->cw.sender_str),
+					    wp->cw.sender_name,
+					    sizeof(wp->cw.sender_name));
+			if (rh == 0) {
+				/* to avoid being fooled by forged Received:
+				 * fields, do not skip unrecognized forms */
+				wp->cw.log_ip_pos = 0;
+
+			} else if (*rh != '\0') {
+				thr_log_print(&wp->cw, 1,
+					      "skip %s Received: header\n", rh);
+
+			} else if (!check_mx_listing(&wp->cw)) {
+				/* put the IP address in the log file
+				 * if now know it */
+				i = strlen(wp->cw.sender_str);
+				if (i > wp->cw.log_ip_len)
+					i = wp->cw.log_ip_len;
+				old_eof = log_lseek_get(&wp->cw);
+				if (old_eof == 0) {
+					;
+				} else if (-1 == lseek(wp->cw.log_fd,
+						       wp->cw.log_ip_pos,
+						       SEEK_SET)) {
+					thr_error_msg(&wp->cw,
+						      "lseek(%s,%d,SEEK_SET):"
+						      " %s",
+						      wp->cw.log_nm,
+						      (int)wp->cw.log_ip_pos,
+						      ERROR_STR());
+				} else {
+					j = write(wp->cw.log_fd,
+						  wp->cw.sender_str, i);
+					/* cannot log errors from that write()
+					 * because the file is at the wrong
+					 * position */
+					lseek(wp->cw.log_fd,
+					      old_eof, SEEK_SET);
+				}
+			}
+		}
+		return SMFIS_CONTINUE;
+	}
+
+	/* remember existing X-DCC headers so that we can delete them */
+	if (chghdr == SETHDR
+	    && (j = f_len - LITZ(DCC_XHDR_START DCC_XHDR_END)) >= 0
+	    && !CLITCMP(headerf, DCC_XHDR_START)
+	    && !CLITCMP(headerf+f_len-LITZ(DCC_XHDR_END), DCC_XHDR_END)) {
+		cp = headerf+LITZ(DCC_XHDR_START);
+		for (i = 0; ; ++i) {
+			if (i >= wp->num_x_dcc) {
+				if (i < NUM_XHDRS) {
+					++wp->num_x_dcc;
+					wp->xhdrs[i].num = 1;
+					wp->xhdrs[i].len = j;
+					memcpy(wp->xhdrs[i].brand, cp, j);
+				}
+				break;
+			}
+
+			if (j == wp->xhdrs[i].len
+			    && !strncasecmp(cp, wp->xhdrs[i].brand, j)) {
+				/* this is a familiar X-DCC header */
+				if (wp->xhdrs[i].num < 255)
+					++wp->xhdrs[i].num;
+				break;
+			}
+		}
+	}
+
+	dcc_ck_get_sub(&wp->cw.cks, headerf, headerv);
+
+	/* Notice MIME multipart boundary definitions */
+	dcc_ck_mime_hdr(&wp->cw.cks, headerf, headerv);
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+static sfsistat
+dccm_eoh(SMFICTX *milter_ctx)
+{
+	WORK *wp;
+
+	wp = get_wp(milter_ctx, GET_WP_GOING);
+
+	/* finish logging the envelope on the first header,
+	 * but if there were no headers we must do it now */
+	if (!(wp->cw.cmn_fgs & CMN_FG_ENV_LOGGED))
+		thr_log_envelope(&wp->cw, 1);
+
+	/* Create a checksum for a null Message-ID header if there
+	 * was no Message-ID header.  */
+	if (wp->cw.cks.sums[DCC_CK_MESSAGE_ID].type != DCC_CK_MESSAGE_ID)
+		dcc_get_cks(&wp->cw.cks, DCC_CK_MESSAGE_ID, "", 0);
+
+	/* log the blank line between the header and the body */
+	log_body_write(&wp->cw, "\n", 1);
+
+	/* Check DNS blacklists for STMP client and envelope sender
+	 * unless DNSBL checks are turned off for all of the recipients */
+	if (wp->cw.cks.dnsbl) {
+		if (wp->cw.cks.sums[DCC_CK_IP].type == DCC_CK_IP)
+			dcc_client_dnsbl(wp->cw.cks.dnsbl, &wp->cw.cks.ip_addr,
+					 wp->cw.sender_name);
+		if (wp->cw.mail_host[0] != '\0')
+			dcc_mail_host_dnsbl(wp->cw.cks.dnsbl, wp->cw.mail_host);
+	}
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+static sfsistat
+dccm_body(SMFICTX *milter_ctx, u_char *bodyp, size_t bodylen)
+{
+	WORK *wp;
+
+	wp = get_wp(milter_ctx, GET_WP_GOING);
+
+	/* Log the body block */
+	log_body_write(&wp->cw, (const char *)bodyp, bodylen);
+
+	dcc_ck_body(&wp->cw.cks, bodyp, bodylen);
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+static void
+msg_fin(SMFICTX *milter_ctx, WORK *wp)
+{
+	dcc_cks_fin(&wp->cw.cks);
+
+	LOG_CAPTION(wp, DCC_LOG_MSG_SEP);
+	thr_log_late(&wp->cw);
+
+	/* get sendmail's final say */
+	ask_sm(milter_ctx, wp);
+
+	/* check the grey and white lists */
+	cmn_ask_white(&wp->cw);
+}
+
+
+
+/* deal with the end of the SMTP message as announced by sendmail */
+static sfsistat
+dccm_eom(SMFICTX *milter_ctx)
+{
+	static char null[] = "";	/* libmilter doesn't know about const */
+	WORK *wp;
+	char *hdr;
+	char delbuf[LITZ(DCC_XHDR_START)+DCC_BRAND_MAXLEN+LITZ(DCC_XHDR_END)+1];
+	int xhdr_fname_len;
+	int i, j;
+
+	wp = get_wp(milter_ctx, GET_WP_GOING);
+
+	msg_fin(milter_ctx, wp);
+
+	/* delete pre-existing X-DCC headers to prevent tricks on MUAs that
+	 * pay attention to them */
+	if (chghdr == SETHDR) {
+		for (i = 0; i < wp->num_x_dcc; ++i) {
+			snprintf(delbuf, sizeof(delbuf), DCC_XHDR_PAT,
+				 wp->xhdrs[i].len, wp->xhdrs[i].brand);
+			do {
+				j = smfi_chgheader(wp->milter_ctx, delbuf,
+						   wp->xhdrs[i].num, null);
+				if (MI_SUCCESS != j) {
+					thr_error_msg(&wp->cw,
+						      "smfi_delheader(\"%s\","
+						      "\"\")=%d",
+						      delbuf, j);
+				}
+			} while (--wp->xhdrs[i].num > 0);
+		}
+	}
+
+	wp->cw.header.buf[0] = '\0';
+	wp->cw.header.used = 0;
+	if (wp->cw.tgts <= wp->cw.white_tgts) {
+		/* it is whitelist for all targets,
+		 * so add X-DCC header saying so */
+		if (chghdr != NOHDR)
+			xhdr_whitelist(&wp->cw.header);
+		xhdr_fname_len = DCC_XHDR_WHITELIST_FNAME_LEN+2;
+
+		/* log it if the target count is high enough */
+		dcc_honor_log_cnts(&wp->cw.ask_st, &wp->cw.cks, wp->cw.tgts);
+
+	} else {
+		/* Report to the DCC
+		 * Request a temporary failure if the DCC failed and we
+		 * are trying hard */
+		i = cmn_ask_dcc(&wp->cw);
+		if (i <= 0) {
+			if (!i && try_extra_hard)
+				return msg_tempfail(wp, &dcc_fail_reply);
+
+			/* after unrecoverable errors without even a fake
+			 * header from local blacklisting, act as if the
+			 * DCC server said not-spam but without a header */
+		}
+		xhdr_fname_len = wp->cw.xhdr_fname_len+2;
+	}
+	/* install the X-DCC header */
+	if (chghdr != NOHDR && wp->cw.header.buf[0] != '\0') {
+		/* kludge the trailing '\n' that sendmail hates */
+		wp->cw.header.buf[wp->cw.header.used-1] = '\0';
+		hdr = &wp->cw.header.buf[xhdr_fname_len];
+		i = smfi_addheader(wp->milter_ctx, wp->cw.xhdr_fname, hdr);
+		if (MI_SUCCESS != i)
+			thr_error_msg(&wp->cw,
+				      "smfi_addheader(\"%s\",\"%s\")=%d",
+				      wp->cw.xhdr_fname, hdr, i);
+		wp->cw.header.buf[wp->cw.header.used-1] = '\n';
+	}
+
+	++totals.msgs;
+	totals.tgts += wp->cw.tgts;
+
+	/* get consensus of targets' wishes */
+	users_process(&wp->cw);
+	/* log the consensus & generate SMTP rejection message if needed */
+	users_log_result(&wp->cw, 0);
+
+	if (wp->cw.ask_st & ASK_ST_GREY_EMBARGO) {
+		totals.tgts_embargoed += wp->cw.tgts;
+		++totals.msgs_embargoed;
+		return msg_reject(wp);
+	}
+
+	/* tell sendmail to deliver it if all (remaining) targets want it */
+	if (wp->cw.reject_tgts == 0) {
+		msg_done(wp, 0);
+		return SMFIS_ACCEPT;
+	}
+
+	/* it is rejectable spam unless we are ignoring results */
+	switch (wp->cw.action) {
+	case CMN_IGNORE:
+		if (wp->cw.reject_tgts != 0) {
+			totals.tgts_ignored += wp->cw.reject_tgts;
+			++totals.msgs_spam;
+		}
+		msg_done(wp, DCC_XHDR_RESULT_I_A);
+		return SMFIS_ACCEPT;
+
+	case CMN_DISCARD:
+		/* discard it if that is our choice
+		 * or if sendmail said to */
+		if (wp->cw.reject_tgts != 0) {
+			totals.tgts_discarded += wp->cw.reject_tgts;
+			++totals.msgs_spam;
+		}
+		msg_done(wp, DCC_XHDR_RESULT_DISCARD);
+		return SMFIS_DISCARD;
+
+	case CMN_REJECT:
+		if (wp->cw.reject_tgts != 0) {
+			totals.tgts_rejected += wp->cw.reject_tgts;
+			++totals.msgs_spam;
+		}
+	}
+
+	/* tell sendmail what to do with it */
+	return msg_reject(wp);
+}
+
+
+
+/* deal with an aborted SMTP transaction */
+static void
+msg_abort(WORK *wp)
+{
+	if (wp->cw.env_from[0] == '\0')
+		return;
+
+	wp->cw.ask_st |= ASK_ST_INVALID_MSG;
+	if (!(wp->cw.cmn_fgs & CMN_FG_ENV_LOGGED))
+		thr_log_envelope(&wp->cw, 0);
+	msg_fin(wp->milter_ctx, wp);
+
+	users_process(&wp->cw);
+	users_log_result(&wp->cw, "STMP message aborted");
+
+	/* create log files for -d
+	 * and without any recipents but with "option log-all" */
+	if (dcc_clnt_debug
+	    || (wp->cw.init_sws & FLTR_SW_LOG_ALL))
+		wp->cw.ask_st |= ASK_ST_LOGIT;
+
+	if (wp->cw.ask_st & ASK_ST_LOGIT)
+		LOG_CAPTION(wp, DCC_XHDR_RESULT"STMP message aborted\n");
+}
+
+
+
+/* end of the SMTP session */
+static sfsistat
+dccm_close(SMFICTX *milter_ctx)
+{
+	int msg_cnt;
+	struct timeval tv;
+	WORK *wp;
+
+	wp = get_wp(milter_ctx, GET_WP_CLOSE);
+	if (!wp) {
+		smfi_setpriv(milter_ctx, 0);
+		return SMFIS_TEMPFAIL;
+	}
+
+	msg_abort(wp);
+
+	/* finished with the context */
+	log_stop(&wp->cw);
+	lock_work();
+	free_rcpt_sts(&wp->cw, 0);
+
+	wp->milter_ctx = WORK_MILTER_CTX_IDLE;
+	wp->fwd = work_free;
+	work_free = wp;
+
+	msg_cnt = work_too_many;
+	if (msg_cnt != 0) {
+		gettimeofday(&tv, 0);
+		if (work_msg_time == tv.tv_sec) {
+			msg_cnt = 0;
+		} else {
+			work_msg_time = tv.tv_sec;
+			work_too_many = 0;
+		}
+	}
+	unlock_work();
+	if (msg_cnt != 0)
+		dcc_error_msg("%d too many simultaneous mail messages",
+			      msg_cnt);
+
+	smfi_setpriv(milter_ctx, 0);
+
+	return SMFIS_CONTINUE;
+}
+
+
+
+static sfsistat
+dccm_abort(SMFICTX *milter_ctx)
+{
+	WORK *wp;
+
+	wp = get_wp(milter_ctx, GET_WP_ABORT);
+	if (!wp)
+		return SMFIS_TEMPFAIL;
+
+	msg_abort(wp);
+
+	/* get ready for possible new message */
+	msg_clear(wp);
+	return SMFIS_CONTINUE;
+}