diff thrlib/cmn.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/thrlib/cmn.c	Tue Mar 10 13:49:58 2009 +0100
@@ -0,0 +1,2523 @@
+/* Distributed Checksum Clearinghouse
+ *
+ * threaded version of client library
+ *
+ * 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.148 $Revision$
+ */
+
+
+#include "cmn_defs.h"
+#include "dcc_paths.h"
+
+CMN_ACTION action = CMN_REJECT;
+
+CHGHDR chghdr = SETHDR;
+
+const char *userdirs;
+static DCC_PATH userdirs_path;
+static int userdirs_len;
+
+u_char dcc_query_only;
+u_char try_extra_hard;			/* 0 or DCC_CLNT_FG_NO_FAIL */
+u_char to_white_only;
+
+u_int dcc_ctxt_sn = 1;			/* change X-DCC header server name */
+
+const char *max_max_work_src = "FD_SETSIZE limit";
+int max_work;
+int init_work;
+int total_work;
+
+static int total_rcpt_sts;
+RCPT_ST *rcpt_st_free;
+
+/* cwf_mutex protects all CWF structures as well as the cmn_wf structure */
+typedef struct cwf {			/* private whitelist state */
+    struct cwf *older, *newer;
+    DCC_WF	wf;
+} CWF;
+static CWF *cur_cwf, cwfs[NUM_CWFS];
+
+/* protected by user_log_mutex */
+static int user_log_fd = -1;
+static int log_fd2 = -1;
+
+
+/* the work lock must be held or not yet exist */
+static void
+add_rcpt_sts(int i)
+{
+	RCPT_ST *rcpt_st;
+
+	total_rcpt_sts += i;
+
+	rcpt_st = dcc_malloc(sizeof(*rcpt_st)*i);
+	memset(rcpt_st, 0, sizeof(*rcpt_st)*i);
+
+	while (i-- != 0) {
+		rcpt_st->fwd = rcpt_st_free;
+		rcpt_st_free = rcpt_st;
+		++rcpt_st;
+	}
+}
+
+
+
+void
+cmn_init(void)
+{
+	init_work = 50;
+	if (init_work > max_max_work)
+		init_work = max_max_work;
+	add_rcpt_sts(init_work);
+
+	finish_replies();
+
+	/* start the client library threads and locks */
+	dcc_clnt_thread_init();
+	for (cur_cwf = cwfs; cur_cwf <= LAST(cwfs); ++cur_cwf) {
+		cur_cwf->newer = cur_cwf-1;
+		cur_cwf->older = cur_cwf+1;
+		dcc_wf_init(&cur_cwf->wf, DCC_WF_PER_USER);
+	}
+	cur_cwf = cwfs;
+	LAST(cwfs)->older = cur_cwf;
+	cur_cwf->newer = LAST(cwfs);
+
+	totals_init();
+}
+
+
+
+void
+cmn_create(CMN_WORK *cwp)
+{
+	cwp->tmp_fd = -1;
+	cwp->log_fd = -1;
+}
+
+
+
+u_char
+cmn_open_tmp(CMN_WORK *cwp)
+{
+	cwp->tmp_fd = dcc_mkstemp(cwp->emsg,
+				  cwp->tmp_nm, sizeof(cwp->tmp_nm),
+				  cwp->id, sizeof(cwp->id),
+				  0, 0, DCC_TMP_LOG_PREFIX, 1, 0);
+
+	return cwp->tmp_fd >= 0;
+}
+
+
+
+void
+cmn_close_tmp(CMN_WORK *cwp)
+{
+	if (cwp->tmp_fd >= 0) {
+		if (0 > close(cwp->tmp_fd))
+			thr_error_msg(cwp, "close(%s): %s",
+				      cwp->tmp_nm, ERROR_STR());
+		cwp->tmp_fd = -1;
+	}
+	cwp->tmp_nm[0] = '\0';
+}
+
+
+
+u_char
+cmn_write_tmp(CMN_WORK *cwp, const void *buf, int len)
+{
+	int i;
+
+	if (cwp->tmp_fd < 0)
+		return 1;
+
+	i = write(cwp->tmp_fd, buf, len);
+	if (i == len)
+		return 1;
+
+	if (i < 0)
+		thr_error_msg(cwp, "write(%s,%d): %s",
+			      cwp->tmp_nm, len, ERROR_STR());
+	else
+		thr_error_msg(cwp, "write(%s,%d)=%d",
+			      cwp->tmp_nm, len, i);
+	cmn_close_tmp(cwp);
+	return 0;
+}
+
+
+
+/* If the immediate SMTP client because it is a listed MX server,
+ *	then we must ignore its IP address and keep looking for the
+ *	real SMTP client. */
+u_char					/* 1=listed MX server */
+check_mx_listing(CMN_WORK *cwp)
+{
+	DCC_TGTS tgts;
+	u_char result;
+
+	lock_wf();
+	result = dcc_white_mx(cwp->emsg, &tgts, &cwp->cks);
+	unlock_wf();
+	if (!result) {
+		thr_error_msg(cwp, "%s", cwp->emsg);
+		return 0;
+	}
+
+	if (tgts == DCC_TGTS_OK) {
+		return 0;
+	}
+
+	if (tgts == DCC_TGTS_SUBMIT_CLIENT) {
+		/* Common SMTP submission clients are too dumb to do the
+		 * right thing with 4yz rejections of individual Rcpt_To
+		 * commands.  So reject the message for all or no recipients. */
+		cwp->cmn_fgs |= CMN_FG_FROM_SUBMIT;
+		thr_log_print(cwp, 1,
+			      "%s is a listed 'submit' client\n",
+			      dcc_trim_ffff(cwp->sender_str));
+		return 0;
+	}
+
+	if (tgts == DCC_TGTS_OK_MXDCC) {
+		thr_log_print(cwp, 1,
+			      "%s is a whitelisted MX server with DCC client\n",
+			      dcc_trim_ffff(cwp->sender_str));
+		cwp->ask_st |= ASK_ST_QUERY;
+	} else if (tgts == DCC_TGTS_OK_MX) {
+		thr_log_print(cwp, 1, "%s is a whitelisted MX server\n",
+			      dcc_trim_ffff(cwp->sender_str));
+	} else {
+		return 0;
+	}
+
+	/* we cannot greylist or reject through our MX servers */
+	cwp->cmn_fgs |= CMN_FG_FROM_MX;
+	if (cwp->action == CMN_REJECT)
+		cwp->action = CMN_DISCARD;
+
+	cwp->sender_name[0] = '\0';
+	cwp->sender_str[0] = '\0';
+	dcc_unget_ip_ck(&cwp->cks);
+
+	/* tell caller to look at the next Received: header */
+	return 1;
+}
+
+
+
+/* clear a common work area for a message, possibly not the first
+ * in the session */
+void
+cmn_clear(CMN_WORK *cwp, struct work *wp,
+	  u_char new_conn)		/* 1=first message for the connection */
+{
+	log_stop(cwp);
+
+	cmn_close_tmp(cwp);
+
+	if (cwp->num_rcpts)
+		free_rcpt_sts(cwp, 1);
+
+	memset(&cwp->CMN_WORK_ZERO, 0,
+	       sizeof(*cwp) - ((char*)&cwp->CMN_WORK_ZERO - (char*)cwp));
+	cwp->cmn_fgs  = 0;
+	cwp->mail_host[0] = '\0';
+	cwp->env_from[0] = '\0';
+	cwp->early_log.len = 0;
+	cwp->emsg[0] = '\0';
+	cwp->id[0] = '\0';
+	cwp->header.used = 0;
+	cwp->cmn_fgs = CMN_FG_LOG_EARLY;
+	if (dcc_query_only)
+		cwp->ask_st |= ASK_ST_QUERY_GREY;
+	cwp->action = action;
+
+	if (new_conn) {
+		cwp->wp = wp;
+		cwp->helo[0] = '\0';
+		cwp->clnt_name[0] = '\0';
+		cwp->clnt_str[0] = '\0';
+	} else {
+		/* assume for now that the sender is the current SMTP client */
+		strcpy(cwp->sender_name, cwp->clnt_name);
+		strcpy(cwp->sender_str, cwp->clnt_str);
+	}
+}
+
+
+
+/* free all of the per-recipient state for a message */
+void
+free_rcpt_sts(CMN_WORK *cwp, u_char need_lock)
+{
+	RCPT_ST *rcpt_st, *next_rcpt_st;
+
+	rcpt_st = cwp->rcpt_st_first;
+	if (!rcpt_st)
+		return;
+
+	if (need_lock)
+		lock_work();
+	cwp->rcpt_st_first = 0;
+	do {
+		next_rcpt_st = rcpt_st->fwd;
+		rcpt_st->fwd = rcpt_st_free;
+		rcpt_st_free = rcpt_st;
+	} while ((rcpt_st = next_rcpt_st) != 0);
+	cwp->num_rcpts = 0;
+
+	if (need_lock)
+		unlock_work();
+}
+
+
+
+RCPT_ST *
+alloc_rcpt_st(CMN_WORK *cwp,
+	   u_char unlocked)		/* 1=unlocked on entry & exit */
+{
+	RCPT_ST *rcpt_st;
+
+	if (cwp->num_rcpts >= MAX_RCPTS) {
+		thr_error_msg(cwp, "too many recipients");
+		return 0;
+	}
+
+	if (unlocked)
+		lock_work();
+	rcpt_st = rcpt_st_free;
+	if (!rcpt_st) {
+		if (dcc_clnt_debug > 1)
+			thr_trace_msg(cwp,
+				      "add %d recipient blocks to %d",
+				      init_work, total_rcpt_sts);
+		add_rcpt_sts(init_work);
+		rcpt_st = rcpt_st_free;
+	}
+	rcpt_st_free = rcpt_st->fwd;
+	if (unlocked)
+		unlock_work();
+
+	rcpt_st->fwd = 0;
+	rcpt_st->log_pos_white = -1;
+	rcpt_st->log_pos_to = -1;
+	memset(rcpt_st->wtgts, 0, sizeof(rcpt_st->wtgts));
+	rcpt_st->env_to_tgts = 0;
+	rcpt_st->user_tgts = 0;
+	rcpt_st->grey_result = ASK_GREY_OFF;
+	rcpt_st->embargo_num = 0;
+	rcpt_st->fgs = 0;
+	rcpt_st->sws = 0;
+	rcpt_st->user[0] = '\0';
+	rcpt_st->rej_msg[0] = '\0';
+	rcpt_st->dir[0] = '\0';
+	rcpt_st->user_log_nm[0] = '\0';
+
+	rcpt_st->cwp = cwp;
+	if (!cwp->rcpt_st_first) {
+		cwp->rcpt_st_first = rcpt_st;
+	} else {
+		cwp->rcpt_st_last->fwd = rcpt_st;
+	}
+	cwp->rcpt_st_last = rcpt_st;
+	++cwp->num_rcpts;
+
+	return rcpt_st;
+}
+
+
+
+void
+parse_userdirs(const char *arg)
+{
+	/* add '/' to end of the path without converting it to "/" */
+	if (*arg == '\0') {
+		userdirs_path[0] = '\0';
+		userdirs_len = 0;
+	} else {
+		strncpy(userdirs_path, arg,
+			sizeof(userdirs_path));
+		userdirs_len = strlen(userdirs_path)-1;
+		while (userdirs_len > 1 && userdirs_path[userdirs_len] == '/')
+			--userdirs_len;
+		userdirs_path[++userdirs_len] = '/';
+		userdirs_path[++userdirs_len] = '\0';
+
+	}
+	userdirs = userdirs_path;
+}
+
+
+
+/* sanitize recipient mailbox and per-user log and whitelist directory */
+u_char					/* 0=complain about something */
+get_user_dir(RCPT_ST *rcpt_st,
+	     const char *str1, int str1_len, const char *str2, int str2_len)
+{
+	char *p;
+	char c;
+	u_char seen_slash;
+	int dots;
+	int i;
+
+	if (!userdirs) {
+		rcpt_st->dir[0] = '\0';
+		return 1;
+	}
+
+	memcpy(rcpt_st->dir, userdirs, userdirs_len);
+	i = userdirs_len;
+	if (i+str1_len < ISZ(rcpt_st->dir))
+		memcpy(&rcpt_st->dir[i], str1, str1_len);
+	i += str1_len;
+	if (str2) {
+		if (i+str2_len+1 < ISZ(rcpt_st->dir)) {
+			rcpt_st->dir[i++] = '/';
+			memcpy(&rcpt_st->dir[i], str2, str2_len);
+		}
+		i += str2_len;
+	}
+	if (i >= ISZ(rcpt_st->dir)) {
+		dcc_pemsg(EX_DATAERR, rcpt_st->cwp->emsg,
+			  "recipient \"%s\" is too long", rcpt_st->dir);
+		rcpt_st->dir[0] = '\0';
+		return 0;
+	}
+	rcpt_st->dir[i] = '\0';
+
+	/* To get a consistent directory name,
+	 * convert ASCII upper to lower case.
+	 * Be simplistic about international character sets and
+	 * avoid locale and portability complications.
+	 * Refuse insecure paths. */
+	seen_slash = 1;			/* userdirs ends with '/' */
+	dots = 0;
+	p = &rcpt_st->dir[userdirs_len];
+	for (;;) {
+		c = *p;
+		if (c == '/' || c == '\\' || c == '\0') {
+			if (dots == 2) {
+				dcc_pemsg(EX_DATAERR, rcpt_st->cwp->emsg,
+					  "path \"%s\" is insecure",
+					  rcpt_st->dir);
+				rcpt_st->dir[0] = '\0';
+				return 0;
+			}
+			if (c == '\0')
+				break;
+			seen_slash = 1;
+			dots = 0;
+		} else if (c == '.' && seen_slash && dots <= 1) {
+			++dots;
+		} else {
+			*p = DCC_TO_LOWER(c);
+			seen_slash = 0;
+			dots = 0;
+		}
+		++p;
+	}
+
+	return 1;
+}
+
+
+
+/* start main log file */
+void
+log_start(CMN_WORK *cwp)
+{
+	char date_buf[40];
+
+	/* don't even whine if there is no log directory */
+	if (dcc_main_logdir[0] == '\0')
+		return;
+
+	/* nothing to do if we already have a log file */
+	if (cwp->log_fd >= 0)
+		return;
+
+	cwp->log_size = 0;
+	cwp->log_fd = dcc_main_log_open(cwp->emsg, cwp->log_nm,
+					cwp->id, sizeof(cwp->id));
+	if (cwp->log_fd < 0) {
+		static time_t whined;
+		time_t now;
+
+		/* complain about not being able to open log files
+		 * only occassionally */
+		now = time(0);
+		if (now < whined || now > whined+5*60 || dcc_clnt_debug)
+			dcc_error_msg("%s", cwp->emsg);
+		whined = now;
+		cwp->emsg[0] = '\0';
+		return;
+	}
+
+	gettimeofday(&cwp->ldate, 0);
+
+	thr_log_print(cwp, 0, DCC_LOG_DATE_PAT"\n",
+		      dcc_time2str(date_buf, sizeof(date_buf), DCC_LOG_DATE_FMT,
+				   cwp->ldate.tv_sec));
+}
+
+
+
+/* get an independent FD for the main log file that can be
+ * repositioned without affecting additional output to the main log. */
+static u_char
+log2_start(CMN_WORK *cwp)
+{
+	DCC_PATH abs_nm;
+
+#ifdef DCC_DEBUG_CLNT_LOCK
+	if (user_log_owner != pthread_self())
+		dcc_logbad(EX_SOFTWARE, "don't have user_log lock");
+#endif
+
+	if (log_fd2 >= 0)
+		return 1;
+
+	/* give up if things are already broken */
+	if (log_fd2 != -1
+	    || cwp->log_fd < 0)
+		return 0;
+
+	/* Some systems don't synchronize the meta data among FDs for
+	 * a file, causing the second FD to appear to be truncated. */
+	if (fsync(cwp->log_fd) < 0)
+		thr_error_msg(cwp, "log_fd fsync(%s): %s",
+			      fnm2abs_err(abs_nm, cwp->log_nm),
+			      ERROR_STR());
+
+	log_fd2 = open(cwp->log_nm, O_RDWR, 0);
+	if (log_fd2 < 0) {
+		thr_error_msg(cwp, "log_fd2 open(%s): %s",
+			      fnm2abs_err(abs_nm, cwp->log_nm),
+			      ERROR_STR());
+		log_fd2 = -2;
+		return 0;
+	}
+
+	return 1;
+}
+
+
+
+static void
+log_fd2_close(int flag)
+{
+	if (user_log_owner == pthread_self()) {
+		if (log_fd2 >= 0)
+			close(log_fd2);
+		log_fd2 = flag;
+	}
+}
+
+
+
+void
+log_stop(CMN_WORK *cwp)
+{
+	thr_log_late(cwp);
+	log_fd2_close(-1);
+
+	if (cwp->log_fd < 0)
+		return;
+
+	/* Close before renaming to accomodate WIN32 foolishness.
+	 * Assuming dcc_mkstemp() works properly, there is no race */
+	dcc_log_close(0, cwp->log_nm, cwp->log_fd, &cwp->ldate);
+	if (!(cwp->ask_st & ASK_ST_LOGIT)) {
+		/* Delete the log file if it is not interesting */
+		unlink(cwp->log_nm);
+	} else {
+		/* give it a permanent name if it is interesting */
+		dcc_log_keep(0, cwp->log_nm);
+	}
+	cwp->log_nm[0] = '\0';
+	cwp->log_fd = -1;
+}
+
+
+
+void
+log_write(CMN_WORK *cwp, const void *buf, u_int buflen)
+{
+	int result;
+
+	if (cwp->log_fd < 0)
+		return;
+
+	if (!buflen)
+		buflen = strlen(buf);
+	cwp->log_size += buflen;
+
+	result = write(cwp->log_fd, buf, buflen);
+	if (buflen == (u_int)result)
+		return;
+
+	if (result < 0)
+		dcc_error_msg("write(%s): %s", cwp->log_nm, ERROR_STR());
+	else
+		dcc_error_msg("write(%s,%d)=%d", cwp->log_nm, buflen, result);
+	dcc_log_close(0, cwp->log_nm, cwp->log_fd, &cwp->ldate);
+	cwp->log_fd = -1;
+}
+
+
+
+void
+log_body_write(CMN_WORK *cwp, const char *buf, u_int buflen)
+{
+	int trimlen;
+	const char *p, *lim;
+
+	if (cwp->log_fd < 0)
+		return;
+
+	/* just write if there is room */
+	trimlen = MAX_LOG_KBYTE*1024 - cwp->log_size;
+	if (trimlen >= (int)buflen) {
+		log_write(cwp, buf, buflen);
+		return;
+	}
+
+	/* do nothing if too much already written */
+	if (trimlen < 0)
+		return;
+
+	/* look for and end-of-line near the end of the buffer
+	 * so that we can make the truncation pretty */
+	lim = buf;
+	p = lim+trimlen;
+	if (trimlen > 90)
+		lim += trimlen-90;
+	while (--p > lim) {
+		if (*p == '\n') {
+			trimlen = p-buf+1;
+			break;
+		}
+	}
+	log_write(cwp, buf, trimlen);
+	if (buf[trimlen-1] != '\n')
+		LOG_CMN_EOL(cwp);
+	LOG_CMN_CAPTION(cwp, DCC_LOG_TRN_MSG_CR);
+	cwp->log_size = MAX_LOG_KBYTE*1024+1;
+}
+
+
+
+off_t
+log_lseek_get(CMN_WORK *cwp)
+{
+	off_t result;
+
+	if (cwp->log_fd < 0)
+		return 0;
+	result = lseek(cwp->log_fd, 0, SEEK_END);
+	if (result == -1) {
+		thr_error_msg(cwp, "lseek(%s, 0, SEEK_END): %s",
+			      cwp->log_nm, ERROR_STR());
+		dcc_log_close(0, cwp->log_nm, cwp->log_fd, &cwp->ldate);
+		cwp->log_fd = -1;
+		return 0;
+	}
+	return result;
+}
+
+
+
+static u_char
+log_lseek_set(CMN_WORK *cwp, off_t pos)
+{
+#ifdef DCC_DEBUG_CLNT_LOCK
+	if (user_log_owner != pthread_self())
+		dcc_logbad(EX_SOFTWARE, "don't have user_log lock");
+#endif
+
+	if (log_fd2 < 0)
+		return 0;
+
+	if (-1 == lseek(log_fd2, pos, SEEK_SET)) {
+		thr_error_msg(cwp, "lseek(%s,%d,SEEK_SET): %s",
+			      cwp->log_nm, (int)pos, ERROR_STR());
+		log_fd2_close(-2);
+		return 0;
+	}
+
+	return 1;
+}
+
+
+
+/* put something into a log file
+ * does not append '\n' */
+static int				/* bytes written */
+vthr_log_print(CMN_WORK *cwp,
+	       u_char error,		/* 1=important enough to buffer */
+	       const char *p, va_list args)
+{
+	char logbuf[LOGBUF_SIZE*2];
+	int i;
+
+	if (cwp->log_fd < 0
+	    || (error && (cwp->cmn_fgs & CMN_FG_LOG_EARLY))) {
+		return dcc_vearly_log(&cwp->early_log, p, args);
+	}
+
+	i = vsnprintf(logbuf, sizeof(logbuf), p, args);
+	if (i < ISZ(logbuf)) {
+		log_write(cwp, logbuf, i);
+		return i;
+	}
+	log_write(cwp, logbuf, sizeof(logbuf));
+	log_write(cwp, "...", 3);
+	return i+3;
+}
+
+
+
+/* does not append '\n' */
+int PATTRIB(3,4)			/* bytes written */
+thr_log_print(void *cwp, u_char error, const char *pat, ...)
+{
+	va_list args;
+	int i;
+
+	va_start(args, pat);
+	i = vthr_log_print(cwp, error, pat, args);
+	va_end(args);
+	return i;
+}
+
+
+
+int					/* bytes written */
+thr_error_msg(void *cwp0, const char *pat, ...)
+{
+	CMN_WORK *cwp = cwp0;
+	va_list args;
+	int i;
+
+	va_start(args, pat);
+	dcc_verror_msg(pat, args);
+	va_end(args);
+
+	va_start(args, pat);
+	i = vthr_log_print(cwp, 1, pat, args);
+	va_end(args);
+	thr_log_print(cwp, 1, "\n");
+
+	cwp->ask_st |= ASK_ST_LOGIT;
+
+	return i+1;
+}
+
+
+
+void
+thr_trace_msg(void *cwp0, const char *p, ...)
+{
+	va_list args;
+
+	va_start(args, p);
+	dcc_vtrace_msg(p, args);
+	va_end(args);
+
+	if (cwp0) {
+		CMN_WORK *cwp = cwp0;
+		va_start(args, p);
+		vthr_log_print(cwp, 1, p, args);
+		va_end(args);
+		thr_log_print(cwp, 1, "\n");
+
+		cwp->ask_st |= ASK_ST_LOGIT;
+	}
+}
+
+
+
+void
+thr_log_late(CMN_WORK *cwp)
+{
+	cwp->cmn_fgs &= ~CMN_FG_LOG_EARLY;
+	if (cwp->early_log.len) {
+		log_write(cwp, cwp->early_log.buf, cwp->early_log.len);
+		cwp->early_log.len = 0;
+	}
+}
+
+
+
+void
+thr_log_envelope(CMN_WORK *cwp, u_char ip_placekeeper)
+{
+	RCPT_ST *rcpt_st;
+	off_t cur_pos;
+	int i;
+
+	cwp->cmn_fgs |= CMN_FG_ENV_LOGGED;
+
+	/* install the sender in blank area in the log file that we skipped */
+	cwp->log_ip_pos = log_lseek_get(cwp) + LITZ(DCC_XHDR_TYPE_IP": ");
+	if (cwp->sender_str[0] != '\0') {
+		/* Dccm will not have computed the checksum
+		 * if there were no envelope Mail_From commands
+		 * On a second message in a session, dccm will not have
+		 * looked at cwp->clnt_addr */
+		if (cwp->cks.sums[DCC_CK_IP].type == DCC_CK_INVALID)
+			dcc_get_ipv6_ck(&cwp->cks, &cwp->clnt_addr);
+		i = thr_log_print(cwp, 0,
+				  DCC_XHDR_TYPE_IP": %s %s\n",
+				  cwp->sender_name, cwp->sender_str);
+
+	} else if (ip_placekeeper) {
+		/* log a blank, place keeping string for the IP address
+		 * so that it can be inserted later */
+		i = thr_log_print(cwp, 0,
+				  DCC_XHDR_TYPE_IP":  %*s\n",
+				  1+1+1+INET6_ADDRSTRLEN, "");
+	} else {
+		i = 0;
+	}
+	i -= LITZ(DCC_XHDR_TYPE_IP":  \n");
+	cwp->log_ip_len = i > 0 ? i : 0;
+
+	/* log HELO value if we have it
+	 * make checksum of it or of null string if we don't */
+	if (cwp->helo[0] != '\0')
+		thr_log_print(cwp, 0, "HELO: %s\n", cwp->helo);
+	dcc_ck_get_sub(&cwp->cks, "helo", cwp->helo);
+
+	if (cwp->env_from[0] != '\0') {
+		LOG_CMN_CAPTION(cwp, DCC_XHDR_TYPE_ENV_FROM": ");
+		log_write(cwp, cwp->env_from, 0);
+		dcc_get_cks(&cwp->cks, DCC_CK_ENV_FROM, cwp->env_from, 1);
+
+		if (cwp->mail_host[0] == '\0')
+			parse_mail_host(cwp->env_from, cwp->mail_host,
+					sizeof(cwp->mail_host));
+
+		LOG_CMN_CAPTION(cwp, "  mail_host=");
+		log_write(cwp, cwp->mail_host, 0);
+		if (cwp->mail_host[0] != '\0')
+			dcc_ck_get_sub(&cwp->cks, "mail_host", cwp->mail_host);
+		LOG_CMN_EOL(cwp);
+	}
+
+	cwp->log_pos_to_first = cur_pos = log_lseek_get(cwp);
+	for (rcpt_st = cwp->rcpt_st_first; rcpt_st; rcpt_st = rcpt_st->fwd) {
+		rcpt_st->log_pos_to = cur_pos;
+		LOG_CMN_CAPTION(cwp, DCC_XHDR_TYPE_ENV_TO": ");
+		log_write(cwp, rcpt_st->env_to, 0);
+		if (rcpt_st->fgs & RCPT_FG_BAD_USERNAME) {
+			LOG_CMN_CAPTION(cwp, "  "DCC_XHDR_MTA_REJECTION"\n");
+		} else {
+			LOG_CMN_CAPTION(cwp, "  addr=");
+			log_write(cwp, rcpt_st->user, 0);
+			LOG_CMN_CAPTION(cwp, "  dir=");
+			log_write(cwp, rcpt_st->dir, 0);
+			LOG_CMN_EOL(cwp);
+		}
+		cur_pos = log_lseek_get(cwp);
+	}
+	cwp->log_pos_to_end = cur_pos;
+
+	/* log the blank line between the log file header and mail message */
+	LOG_CMN_EOL(cwp);
+}
+
+
+
+/* open the connection to the nearest DCC server */
+u_char
+ck_dcc_ctxt(CMN_WORK *cwp)
+{
+	if (cwp->dcc_ctxt_sn != dcc_ctxt_sn) {
+		cwp->dcc_ctxt_sn = dcc_ctxt_sn;
+		cwp->dcc_ctxt = dcc_clnt_init(cwp->emsg, cwp->dcc_ctxt, 0,
+					      DCC_CLNT_FG_BAD_SRVR_OK
+					      | DCC_CLNT_FG_NO_PICK_SRVR
+					      | DCC_CLNT_FG_NO_FAIL);
+		if (!cwp->dcc_ctxt) {
+			/* failed to create context */
+			thr_error_msg(cwp, "%s", cwp->emsg);
+			cwp->dcc_ctxt_sn = 0;
+			return 0;
+		}
+		cwp->xhdr_fname_len = get_xhdr_fname(cwp->xhdr_fname,
+						     sizeof(cwp->xhdr_fname),
+						     dcc_clnt_info);
+	}
+	return 1;
+}
+
+
+
+/* find and lock a per-user DCC_WF
+ *	it is locked by grabbing the mutex for the main whiteclnt file */
+static CWF *
+find_cwf(RCPT_ST *rcpt_st)
+{
+	CWF *cwf;
+	DCC_PATH white_nm_buf;
+	const char *white_nm_ptr;
+
+	if (rcpt_st->dir[0] == '\0') {
+		rcpt_st->fgs |= RCPT_FG_NULL_WHITECLNT;
+		return 0;
+	}
+
+	/* canonicalize the key */
+	if (!fnm2rel(white_nm_buf, rcpt_st->dir, "/whiteclnt")) {
+		thr_error_msg(rcpt_st->cwp,
+			      "long user whiteclnt name \"%s/whiteclnt\"",
+			      rcpt_st->dir);
+		rcpt_st->fgs |= RCPT_FG_NULL_WHITECLNT;
+		return 0;
+	}
+	white_nm_ptr = path2fnm(white_nm_buf);
+
+	lock_wf();
+	cwf = cur_cwf;
+	for (;;) {
+		if (!strcmp(white_nm_ptr, cwf->wf.ascii_nm))
+			break;		/* found old DCC_WF for target file */
+
+		if (cwf->older == cur_cwf) {
+			/* We do not know this file.
+			 * Recycle the oldest DCC_WF */
+			if (!dcc_new_white_nm(rcpt_st->cwp->emsg, &cwf->wf,
+					      white_nm_ptr)) {
+				thr_error_msg(rcpt_st->cwp, "%s",
+					      rcpt_st->cwp->emsg);
+				unlock_wf();
+				rcpt_st->fgs |= RCPT_FG_NULL_WHITECLNT;
+				return 0;
+			}
+			break;
+		}
+
+		cwf = cwf->older;
+	}
+
+	/* move to front */
+	if (cwf != cur_cwf) {
+		cwf->older->newer = cwf->newer;
+		cwf->newer->older = cwf->older;
+		cwf->older = cur_cwf;
+		cwf->newer = cur_cwf->newer;
+		cwf->newer->older = cwf;
+		cwf->older->newer = cwf;
+		cur_cwf = cwf;
+	}
+
+	switch (dcc_rdy_white(rcpt_st->cwp->emsg, &cwf->wf, &cmn_tmp_wf)) {
+	case DCC_WHITE_CONTINUE:
+		thr_error_msg(rcpt_st->cwp, "%s", rcpt_st->cwp->emsg);
+		/* fall through */
+	case DCC_WHITE_OK:
+		/* notice if the file contains no checksums or CIDR blocks
+		 * or flag bits that make the file differ from an empty
+		 * or non-existent file */
+		if (cwf->wf.wtbl->hdr.entries == 0
+		    && cwf->wf.wtbl->hdr.cidr.len == 0
+		    && !(cwf->wf.wtbl_flags & DCC_WHITE_FG_TRAPS)) {
+			rcpt_st->fgs |= RCPT_FG_NULL_WHITECLNT;
+		} else {
+			rcpt_st->fgs &= ~RCPT_FG_NULL_WHITECLNT;
+			memcpy(rcpt_st->wf_sum, cwf->wf.wtbl->hdr.ck_sum,
+			       sizeof(rcpt_st->wf_sum));
+		}
+		return cwf;
+	case DCC_WHITE_NOFILE:
+		break;
+	case DCC_WHITE_SILENT:
+		if (dcc_clnt_debug)
+			thr_error_msg(rcpt_st->cwp, "%s", rcpt_st->cwp->emsg);
+		break;
+	case DCC_WHITE_COMPLAIN:
+		thr_error_msg(rcpt_st->cwp, "%s", rcpt_st->cwp->emsg);
+		break;
+	}
+
+	unlock_wf();
+	rcpt_st->fgs |= RCPT_FG_NULL_WHITECLNT;
+	return 0;
+}
+
+
+
+/* digest the results of one recipient's whitelist */
+static void
+white_results(RCPT_ST *rcpt_st,
+	      CMN_WORK *cwp,
+	      RCPT_FGS *fgsp,
+	      DCC_WHITE_RESULT result,
+	      const DCC_WHITE_LISTING *listingp)
+{
+
+	DCC_WHITE_LISTING listing;
+
+	/* call-by-reference parameter to resolve order of evaluation
+	 * in callers */
+	listing = *listingp;
+
+	/* override if the result of the whitelist lookup was bad */
+	switch (result) {
+	case DCC_WHITE_OK:
+		break;
+	case DCC_WHITE_SILENT:
+	case DCC_WHITE_NOFILE:
+		listing = DCC_WHITE_UNLISTED;
+		break;
+	case DCC_WHITE_COMPLAIN:
+	case DCC_WHITE_CONTINUE:
+		thr_error_msg(cwp, "%s", cwp->emsg);
+		return;
+	}
+
+	switch (listing) {
+	case DCC_WHITE_USE_DCC:
+		/* "OK2" for the env_to checksum in the local whitelist
+		 * does not mean the address is half whitelisted,
+		 * but that it is ok to reject or discard spam for it based
+		 * on DCC results.
+		 * It is the original, deprecated mechanism for turn DCC checks
+		 * on and off for individual targets
+		 * We get this value only from dcc_white_sum() */
+		if (rcpt_st)
+			rcpt_st->sws &= ~FLTR_SW_DCC_OFF;
+		/* fall through */
+	case DCC_WHITE_UNLISTED:
+		/* a spam trap rejects everything
+		 * or accepts but marks everything as spam */
+		if (rcpt_st && (rcpt_st->sws & FLTR_SW_TRAPS)) {
+			if (!(*fgsp & RCPT_FG_WHITE))
+				*fgsp |= RCPT_FG_BLACK;
+			/* remember this hit for the log */
+			*fgsp |= RCPT_FG_WLIST_ISSPAM;
+			cwp->rcpt_fgs |= RCPT_FG_WLIST_ISSPAM;
+		}
+		break;
+
+	case DCC_WHITE_LISTED:
+		*fgsp |= RCPT_FG_WHITE;
+		*fgsp &= ~RCPT_FG_BLACK;
+		/* remember this hit for the log */
+		*fgsp |= RCPT_FG_WLIST_NOTSPAM;
+		cwp->rcpt_fgs |= RCPT_FG_WLIST_NOTSPAM;
+		break;
+
+	case DCC_WHITE_BLACK:
+		if (!(*fgsp & RCPT_FG_WHITE))
+			*fgsp |= RCPT_FG_BLACK;
+		/* remember this hit for the log */
+		*fgsp |= RCPT_FG_WLIST_ISSPAM;
+		cwp->rcpt_fgs |= RCPT_FG_WLIST_ISSPAM;
+		break;
+	}
+}
+
+
+
+static void
+rcpt_fgs2ask_st(CMN_WORK *cwp, FLTR_SWS sws, RCPT_FGS fgs)
+{
+	/* We need to know if all targets are whitelisted for the DCC
+	 * before we ask the DCC server.  Mail sent only to whitelisted
+	 * targets should not be reported to the DCC server.
+	 * For that we need a count of whitelisted targets */
+	if (fgs & RCPT_FG_WHITE) {
+		++cwp->white_tgts;
+		return;
+	}
+
+	/* it is spam if it is blacklisted by any target */
+	if (fgs & RCPT_FG_BLACK) {
+		cwp->ask_st |= (ASK_ST_CLNT_ISSPAM | ASK_ST_LOGIT);
+		return;
+	}
+
+	/* If we had a DNS blacklist hit
+	 * and if this recipient believes the DNS blacklist,
+	 * then it is spam to report to DCC server.
+	 * We need to know if it is spam for at least one target before
+	 * deciding what to do for each target. */
+	if (0 != (FLTR_SW_DNSBL_BITS(sws) & ASK_ST_DNSBL_HIT_BITS(cwp->ask_st)))
+		cwp->ask_st |= (ASK_ST_CLNT_ISSPAM | ASK_ST_LOGIT);
+}
+
+
+
+/* set the global defaults for the switches or options */
+static void
+rcpt_sws_def(CMN_WORK *cwp, u_char locked)
+{
+	if (!locked)
+		lock_wf();
+
+	if (to_white_only)
+		cwp->init_sws |= FLTR_SW_DCC_OFF;
+	cwp->init_sws |= FLTR_SW_NO_DISCARD;
+	cwp->init_sws = wf2sws(cwp->init_sws, &cmn_wf);
+	if (cannot_discard)		/* enforce the default if necessary */
+		cwp->init_sws |= FLTR_SW_NO_DISCARD;
+	cwp->rcpts_sws = cwp->init_sws;
+
+	if (!locked)
+		unlock_wf();
+}
+
+
+
+/* merge global and per-user thresholds */
+static void
+make_tholds(DCC_CKSUM_THOLDS out, CWF *cwf)
+{
+	dcc_merge_tholds(out, dcc_tholds_rej, cmn_wf.wtbl);
+
+	if (cwf && cwf->wf.wtbl)
+		dcc_merge_tholds(out, out, cwf->wf.wtbl);
+}
+
+
+
+/* set per-user switches and compute env_to whitelisting
+ *	If we return a pointer, then we grabbed and have kept the lock */
+static CWF *
+rcpt_sws_env_to(CMN_WORK *cwp, RCPT_ST *rcpt_st)
+{
+	CWF *cwf;
+	DCC_WHITE_LISTING listing;
+
+	/* We are finished after finding the recipient's whitelist if we
+	 * have already checked its settings.
+	 *
+	 * In this case, we have been here before for this recipient
+	 * and fetched the global as well as this recipient's switch settings
+	 * and action.
+	 *
+	 * If we found that the file was empty of checksums or non-existent
+	 * before, then assume it is still is and so repeat the previous 0
+	 * answer without wasting time looking.  This is an extremely
+	 * common case that deserves optimizing.  */
+	if (rcpt_st->fgs & RCPT_FG_NULL_WHITECLNT)
+		return 0;
+
+	/* after mapping the per-recipient whiteclnt file,
+	 * do not repeat the work of examining it if we have already done it */
+	cwf = find_cwf(rcpt_st);
+	if (rcpt_st->sws & FLTR_SW_SET)
+		return cwf;
+
+	/* The first time for the first recipient,
+	 * we must get the global switch settings.
+	 * If we have a per-user whitelist, then we have locked cmn_wf
+	 * to protect the per-user whitelist data structures */
+	if (!(cwp->init_sws & FLTR_SW_SET))
+		rcpt_sws_def(cwp, cwf != 0);
+
+	/* get flags and filter settings for recipient,
+	 * including setting FLTR_SW_SET so that we won't do this again */
+	if (cwf)
+		rcpt_st->sws = wf2sws(cwp->init_sws, &cwf->wf);
+	else
+		rcpt_st->sws = cwp->init_sws;
+	if (cannot_discard)		/* dccifd cannot discard */
+		rcpt_st->sws |= FLTR_SW_NO_DISCARD;
+
+	/* if we have a per-user whitelist, then we have already locked cmn_wf
+	 * to protect the per-user whitelist data structures */
+	if (!cwf)
+		lock_wf();
+
+	/* check the env_to address in the global whitelist */
+	dcc_str2ck(rcpt_st->env_to_sum, 0, 0, rcpt_st->env_to);
+	rcpt_st->global_env_to_fgs = 0;
+	white_results(rcpt_st, cwp, &rcpt_st->global_env_to_fgs,
+		      dcc_white_sum(cwp->emsg, &cmn_wf,
+				    DCC_CK_ENV_TO, rcpt_st->env_to_sum,
+				    &rcpt_st->env_to_tgts, &listing),
+		      &listing);
+	if (listing != DCC_WHITE_UNLISTED)
+		cwp->cmn_fgs |= CMN_FG_LOG_ENV_TO;
+
+	/* check the mailbox name (after aliases etc.) in the global whitelist
+	 * if we did not just check it as the envelope Rcpt_To value */
+	if (rcpt_st->user[0] != '\0'
+	    && strcmp(rcpt_st->env_to, rcpt_st->user)) {
+		dcc_str2ck(rcpt_st->user_sum, 0, 0, rcpt_st->user);
+		white_results(rcpt_st, cwp, &rcpt_st->global_env_to_fgs,
+			      dcc_white_sum(cwp->emsg, &cmn_wf,
+					    DCC_CK_ENV_TO, rcpt_st->user_sum,
+					    &rcpt_st->user_tgts, &listing),
+			      &listing);
+		if (listing != DCC_WHITE_UNLISTED)
+			cwp->cmn_fgs |= CMN_FG_LOG_ENV_TO;
+	}
+	if (!cwf) {
+		unlock_wf();
+
+	} else {
+		/* save per-user envelope Rcpt_To value and mailbox name
+		 * (after aliases etc.) white- or blacklisting
+		 * and the DCC_WHITE_USE_DCC setting */
+		rcpt_st->env_to_fgs = 0;
+		white_results(rcpt_st, cwp, &rcpt_st->env_to_fgs,
+			      dcc_white_sum(cwp->emsg, &cwf->wf,
+					    DCC_CK_ENV_TO, rcpt_st->env_to_sum,
+					    &rcpt_st->env_to_tgts, &listing),
+			      &listing);
+		if (rcpt_st->user[0] != '\0'
+		    && strcmp(rcpt_st->env_to, rcpt_st->user)) {
+			white_results(rcpt_st, cwp, &rcpt_st->env_to_fgs,
+				      dcc_white_sum(cwp->emsg, &cwf->wf,
+						    DCC_CK_ENV_TO,
+						    rcpt_st->user_sum,
+						    &rcpt_st->user_tgts,
+						    &listing),
+				      &listing);
+		}
+	}
+
+	/* remove any reset _ON bits from the consensus */
+	cwp->rcpts_sws &= (rcpt_st->sws | ~FLTR_SWS_SETTINGS_ON);
+	/* add any set _OFF bits to the consensus */
+	cwp->rcpts_sws |= (rcpt_st->sws & FLTR_SWS_SETTINGS_OFF);
+
+	return cwf;
+}
+
+
+
+/* see if a recipient's whitelist decision is certain to be the same
+ * as all preceding recipients */
+u_char					/* 0=must reject this recipient */
+cmn_compat_whitelist(CMN_WORK *cwp,
+		     RCPT_ST *rcpt_st_new)
+{
+	RCPT_ST *rcpt_st2;
+	CWF *cwf;
+	FLTR_SWS save_rcpts_sws;
+	DCC_CKSUM_THOLDS tholds_rej;
+
+	/* everything is compatible if we won't reject
+	 * or if we are forced to reject for all if we reject for any */
+	if ((cwp->action != CMN_REJECT)
+	    || cannot_reject
+	    || (cwp->cmn_fgs & CMN_FG_FROM_SUBMIT))
+		return 1;
+
+	/* postpone poking at whitelists on the first not-rejected recipient */
+	rcpt_st2 = cwp->rcpt_st_first;
+	for (;;) {
+		/* wait until later if this is the first recipient  */
+		if (rcpt_st2 == rcpt_st_new)
+			return 1;
+
+		if (!(rcpt_st2->fgs & (RCPT_FG_REJ_FILTER
+				       | RCPT_FG_BAD_USERNAME)))
+			break;
+		rcpt_st2 = rcpt_st2->fwd;
+		if (!rcpt_st2)
+			return 1;
+	}
+
+	/* we are dealing with a second recipient
+	 * get the settings that we postponed when we saw the first recipient */
+	if (!(rcpt_st2->sws & FLTR_SW_SET)) {
+		cwf = rcpt_sws_env_to(cwp, rcpt_st2);
+		make_tholds(cwp->cks.tholds_rej, cwf);
+		cwp->cmn_fgs |= CMN_FG_THOLDS_SET;
+		if (cwf)
+			unlock_wf();
+	}
+
+	/* See if this message might be accepted for this recipient
+	 * but rejected for some other recipient that does not want
+	 * forced discarding or if this recipient does not want forced
+	 * discarding and has weaker filtering than other recipients.
+	 * If so, reject this recipient. */
+
+	save_rcpts_sws = cwp->rcpts_sws;
+	cwf = rcpt_sws_env_to(cwp, rcpt_st_new);
+	make_tholds(tholds_rej, cwf);
+	if (cwf)
+		unlock_wf();
+
+	/* Differing DCC thresholds conflict */
+	if (memcmp(tholds_rej, cwp->cks.tholds_rej, sizeof(tholds_rej))) {
+		cwp->rcpts_sws = save_rcpts_sws;
+		cwp->ask_st |= ASK_ST_LOGIT;
+		rcpt_st_new->fgs |= RCPT_FG_INCOMPAT_REJ;
+		return 0;
+	}
+
+	/* Accept-traps prefer to avoid all rejections including Mail_From
+	 * commands and implicitly discard but tolerate rejections.
+	 * Reject-traps require rejections. */
+	if (rcpt_st_new->sws & FLTR_SW_TRAP_ACC)
+		return 1;
+
+	/* everything other than differing thesholds is compatible
+	 * if discarding is ok for all recipients so far */
+	if (!(cwp->rcpts_sws & FLTR_SW_NO_DISCARD))
+		return 1;
+
+	do {
+		/* ignore already rejected recipients */
+		if (rcpt_st2->fgs & (RCPT_FG_REJ_FILTER | RCPT_FG_BAD_USERNAME))
+			continue;
+
+		/* Traps are fexible. */
+		if (rcpt_st2->sws & FLTR_SW_TRAP_ACC)
+			continue;
+
+		/* Differing whitelists make it possible that the message
+		 * could need to be rejected for one recipient but accepted
+		 * for the other */
+		if (((rcpt_st2->sws & FLTR_SW_NO_DISCARD)
+		     || (rcpt_st_new->sws & FLTR_SW_NO_DISCARD))
+		    && (((rcpt_st2->fgs ^ rcpt_st_new->fgs)
+			 & RCPT_FG_NULL_WHITECLNT) != 0
+			|| (!(rcpt_st2->fgs & RCPT_FG_NULL_WHITECLNT)
+			    && memcmp(rcpt_st2->wf_sum, rcpt_st_new->wf_sum,
+				      sizeof(rcpt_st2->wf_sum))))) {
+			cwp->rcpts_sws = save_rcpts_sws;
+			cwp->ask_st |= ASK_ST_LOGIT;
+			rcpt_st_new->fgs |= RCPT_FG_INCOMPAT_REJ;
+			return 0;
+		}
+
+		/* Stronger filter choices for a preceding recipient are
+		 * potential reasons to reject the message not shared by the
+		 * new recipient and that could force the message to be
+		 * discarded for the preceding recipient */
+		if ((rcpt_st2->sws & FLTR_SW_NO_DISCARD)
+		    && ((FLTR_SWS_ON(rcpt_st2->sws)
+			 & ~FLTR_SWS_ON(rcpt_st_new->sws)) != 0)) {
+			cwp->rcpts_sws = save_rcpts_sws;
+			cwp->ask_st |= ASK_ST_LOGIT;
+			rcpt_st_new->fgs |= RCPT_FG_INCOMPAT_REJ;
+			return 0;
+		}
+
+		/* weaker filter choices for a preceding recipient imply
+		 * potential reasons to reject the message not shared by the
+		 * preceding recipient and that could force the message to
+		 * be discarded for the new recipient */
+		if ((rcpt_st_new->sws & FLTR_SW_NO_DISCARD)
+		    && ((~FLTR_SWS_ON(rcpt_st2->sws)
+			 & FLTR_SWS_ON(rcpt_st_new->sws)) != 0)) {
+			cwp->rcpts_sws = save_rcpts_sws;
+			cwp->ask_st |= ASK_ST_LOGIT;
+			rcpt_st_new->fgs |= RCPT_FG_INCOMPAT_REJ;
+			return 0;
+		}
+	} while ((rcpt_st2 = rcpt_st2->fwd) != rcpt_st_new);
+
+	return 1;
+}
+
+
+
+/* check the whitelists for a single user or target */
+static void
+ask_white_rcpt(CMN_WORK *cwp,
+	       RCPT_ST *rcpt_st,
+	       RCPT_FGS global_fgs)
+{
+	DCC_WHITE_LISTING listing;
+	CWF *cwf;
+	DCC_PATH abs_nm;
+
+	rcpt_st->log_pos_white = log_lseek_get(cwp);
+
+	/* Quit after capturing the log position if the recipient has
+	 * been rejected.
+	 * We cannot do more because dccm does not have the mailer and so
+	 * cannot find the likely userdirs/local/user/whiteclnt file */
+	if (rcpt_st->fgs & (RCPT_FG_REJ_FILTER | RCPT_FG_BAD_USERNAME))
+		return;
+
+	/* Get the switch settings and the env_to whitelist results. */
+	cwf = rcpt_sws_env_to(cwp, rcpt_st);
+	if (!(cwp->cmn_fgs & CMN_FG_THOLDS_SET)) {
+		cwp->cmn_fgs |= CMN_FG_THOLDS_SET;
+		make_tholds(cwp->cks.tholds_rej, cwf);
+	}
+
+	/* Compute white- or blacklisting.
+	 *	The per-user whiteclnt file overrides the global file.
+	 *	The whiteclnt files override the MTA with "option MTA-first".
+	 *	    Without, the MTA controls.
+	 *	Within each category, whitelisting overrides blacklisting.
+	 *
+	 *	The global whitelist's answer for message's checksums other
+	 *	    than the env_to checksums have already been computed in
+	 *	    global_fgs. */
+
+	if (global_fgs != 0) {
+		rcpt_st->fgs &= ~(RCPT_FG_WHITE | RCPT_FG_BLACK);
+		rcpt_st->fgs |= global_fgs;
+	}
+
+	if (rcpt_st->sws & FLTR_SW_MTA_FIRST) {
+		if (cwp->ask_st & ASK_ST_MTA_NOTSPAM) {
+			rcpt_st->fgs |= RCPT_FG_WHITE;
+			rcpt_st->fgs &= ~RCPT_FG_BLACK;
+		} else if (cwp->ask_st & ASK_ST_MTA_ISSPAM) {
+			rcpt_st->fgs |= RCPT_FG_BLACK;
+			rcpt_st->fgs &= ~RCPT_FG_WHITE;
+		}
+	}
+
+	/* apply the previously computed env_to global whitelist results
+	 * as well as the other global whitelist results */
+	if ((rcpt_st->global_env_to_fgs | global_fgs) & RCPT_FG_WHITE) {
+		rcpt_st->fgs &= ~RCPT_FG_BLACK;
+		rcpt_st->fgs |= RCPT_FG_WHITE;
+	} else if ((rcpt_st->global_env_to_fgs | global_fgs) & RCPT_FG_BLACK) {
+		rcpt_st->fgs &= ~RCPT_FG_WHITE;
+		rcpt_st->fgs |= RCPT_FG_BLACK;
+	}
+
+	if (!cwf) {
+		/* Without a per-user whitelist or without any entries in
+		 * the per-user whitelist, we will be using the
+		 * global whitelist for the other messages checksums.
+		 * Arrange to include those results in the per-user log file */
+		memcpy(rcpt_st->wtgts, cwp->wtgts, sizeof(rcpt_st->wtgts));
+
+	} else {
+		/* Check other message checksums in per-user whitelist */
+		white_results(rcpt_st, cwp, &rcpt_st->env_to_fgs,
+			      dcc_white_cks(cwp->emsg, &cwf->wf, &cwp->cks,
+					    rcpt_st->wtgts, &listing),
+			      &listing);
+
+		if (rcpt_st->env_to_fgs == 0) {
+			/* Without an answer from the per-user whitelist,
+			 * we will be using the global whitelist.
+			 * So arrange to include those results in the per-user
+			 * log file */
+			memcpy(rcpt_st->wtgts, cwp->wtgts,
+			       sizeof(rcpt_st->wtgts));
+
+		} else {
+			thr_log_print(cwp, 0, "%s%s\n",
+				      fnm2abs_err(abs_nm, cwf->wf.ascii_nm),
+				      (rcpt_st->env_to_fgs & RCPT_FG_WHITE)
+				      ? DCC_XHDR_ISOK
+				      : (rcpt_st->sws & FLTR_SW_TRAP_ACC)
+				      ? "-->"DCC_XHDR_TRAP_ACC
+				      : (rcpt_st->sws & FLTR_SW_TRAP_REJ)
+				      ? "-->"DCC_XHDR_TRAP_REJ
+				      : DCC_XHDR_ISSPAM);
+			rcpt_st->fgs &= ~(RCPT_FG_WHITE | RCPT_FG_BLACK);
+			rcpt_st->fgs |= rcpt_st->env_to_fgs;
+		}
+
+		/* release common lock that protected the per-user whitelist
+		 * because we are finished with the per-user whitelist */
+		unlock_wf();
+	}
+
+	if (rcpt_st->env_to_tgts != 0
+	    || rcpt_st->user_tgts != 0)
+		cwp->cmn_fgs |= CMN_FG_LOG_ENV_TO;
+
+	if (!(rcpt_st->sws & FLTR_SW_MTA_FIRST)) {
+		if (cwp->ask_st & ASK_ST_MTA_NOTSPAM) {
+			rcpt_st->fgs |= RCPT_FG_WHITE;
+			rcpt_st->fgs &= ~RCPT_FG_BLACK;
+		} else if (cwp->ask_st & ASK_ST_MTA_ISSPAM) {
+			rcpt_st->fgs |= RCPT_FG_BLACK;
+			rcpt_st->fgs &= ~RCPT_FG_WHITE;
+		}
+	}
+}
+
+
+
+/* check the whitelists for all targets */
+void
+cmn_ask_white(CMN_WORK *cwp)
+{
+	RCPT_ST *rcpt_st;
+	RCPT_FGS global_fgs;
+	DCC_OPS grey_op;
+	DCC_WHITE_LISTING listing;
+
+	/* log sendmail access_db spam */
+	if (cwp->ask_st & ASK_ST_MTA_ISSPAM)
+		cwp->ask_st |= ASK_ST_LOGIT;
+
+	dcc_dnsbl_result(&cwp->ask_st, cwp->cks.dnsbl);
+
+	cwp->log_pos_white_first = log_lseek_get(cwp);
+
+	/* Use the main whitelist only for recipients whose individual
+	 * whitelists don't give a black or white answer.
+	 * Check the main whitelist first (and so even if not necessary)
+	 * so that problems with it are in all of the logs and to simplify
+	 * merging the global and per-user whitelist results. */
+	lock_wf();
+	global_fgs = 0;
+	white_results(0, cwp, &global_fgs,
+		      dcc_white_cks(cwp->emsg, &cmn_wf, &cwp->cks,
+				    cwp->wtgts, &listing),
+		      &listing);
+
+	/* get the defaults for the options */
+	if (!(cwp->init_sws & FLTR_SW_SET))
+		rcpt_sws_def(cwp, 1);
+	unlock_wf();
+
+	/* kludge similar to ask_white_rcpt() for no recipients for dccifd with
+	 * the ASCII protocol */
+	if ((rcpt_st = cwp->rcpt_st_first) == 0) {
+		if (cwp->init_sws & FLTR_SW_MTA_FIRST) {
+			if (cwp->ask_st & ASK_ST_MTA_NOTSPAM) {
+				cwp->rcpt_fgs |= RCPT_FG_WHITE;
+				cwp->rcpt_fgs &= ~RCPT_FG_BLACK;
+			} else if (cwp->ask_st & ASK_ST_MTA_ISSPAM) {
+				cwp->rcpt_fgs |= RCPT_FG_BLACK;
+				cwp->rcpt_fgs &= ~RCPT_FG_WHITE;
+			}
+		}
+		if (global_fgs != 0) {
+			cwp->rcpt_fgs &= ~(RCPT_FG_WHITE | RCPT_FG_BLACK);
+			cwp->rcpt_fgs |= global_fgs;
+		}
+		if (!(cwp->init_sws & FLTR_SW_MTA_FIRST)) {
+			if (cwp->ask_st & ASK_ST_MTA_NOTSPAM) {
+				cwp->rcpt_fgs |= RCPT_FG_WHITE;
+				cwp->rcpt_fgs &= ~RCPT_FG_BLACK;
+			} else if (cwp->ask_st & ASK_ST_MTA_ISSPAM) {
+				cwp->rcpt_fgs |= RCPT_FG_BLACK;
+				cwp->rcpt_fgs &= ~RCPT_FG_WHITE;
+			}
+		}
+
+		rcpt_fgs2ask_st(cwp, cwp->init_sws, cwp->rcpt_fgs);
+	}
+
+	for (; rcpt_st; rcpt_st = rcpt_st->fwd) {
+		/* maybe this recipient is whitelisted or a spam trap
+		 * or has per-user option settings */
+		ask_white_rcpt(cwp, rcpt_st, global_fgs);
+
+		rcpt_fgs2ask_st(cwp, rcpt_st->sws, rcpt_st->fgs);
+
+		/* no greylist check if it is off or should not be done */
+		if ((rcpt_st->sws & (FLTR_SW_GREY_OFF | FLTR_SW_TRAPS))
+		    || (cwp->cmn_fgs & (CMN_FG_FROM_MX | CMN_FG_FROM_SUBMIT))
+		    || (rcpt_st->fgs & (RCPT_FG_REJ_FILTER
+					| RCPT_FG_BAD_USERNAME))
+		    || (cwp->ask_st & ASK_ST_INVALID_MSG))
+			continue;
+
+		if (rcpt_st->fgs & RCPT_FG_BLACK) {
+			if (cwp->cks.sums[DCC_CK_IP].type != DCC_CK_IP
+			    || (cwp->cks.sums[DCC_CK_ENV_FROM].type
+				!= DCC_CK_ENV_FROM))
+				continue;
+			grey_op = DCC_OP_GREY_QUERY;
+		} else if (cwp->ask_st & ASK_ST_QUERY_GREY) {
+			grey_op = DCC_OP_GREY_QUERY;
+		} else if (rcpt_st->fgs & RCPT_FG_WHITE) {
+			grey_op = DCC_OP_GREY_WHITE;
+		} else {
+			grey_op = DCC_OP_GREY_REPORT;
+		}
+		rcpt_st->grey_result = ask_grey(cwp->emsg, cwp->dcc_ctxt,
+						grey_op,
+						rcpt_st->msg_sum,
+						rcpt_st->triple_sum,
+						&cwp->cks,
+						rcpt_st->env_to_sum,
+						&rcpt_st->embargo_num,
+						&cwp->early_grey_tgts,
+						&cwp->late_grey_tgts);
+
+		switch (rcpt_st->grey_result) {
+		case ASK_GREY_OFF:
+		case ASK_GREY_SPAM:
+			dcc_logbad(EX_SOFTWARE,
+				   "cmn_ask_white grey_result=%d",
+				   rcpt_st->grey_result);
+			break;
+		case ASK_GREY_FAIL:
+			thr_error_msg(cwp, "%s", cwp->emsg);
+			/* If we are trying hard, assume the
+			 * message would have been embargoed */
+			if (try_extra_hard)
+				cwp->ask_st |= (ASK_ST_GREY_EMBARGO
+						| ASK_ST_GREY_LOGIT
+						| ASK_ST_LOGIT);
+			break;
+		case ASK_GREY_EMBARGO:
+			if (rcpt_st->embargo_num == 1
+			    && (rcpt_st->fgs & RCPT_FG_BLACK)) {
+				/* don't bother revoking non-existent entry */
+				rcpt_st->grey_result = ASK_GREY_OFF;
+			} else {
+				cwp->ask_st |= (ASK_ST_GREY_EMBARGO
+						| ASK_ST_GREY_LOGIT
+						| ASK_ST_LOGIT);
+				if (cwp->max_embargo_num < rcpt_st->embargo_num)
+				    cwp->max_embargo_num = rcpt_st->embargo_num;
+			}
+			break;
+		case ASK_GREY_EMBARGO_END:
+			cwp->ask_st |= (ASK_ST_GREY_LOGIT | ASK_ST_LOGIT);
+			cwp->rcpt_fgs |= RCPT_FG_GREY_END;
+			break;
+		case ASK_GREY_PASS:
+			break;
+		case ASK_GREY_WHITE:
+			rcpt_st->fgs |= RCPT_FG_GREY_WHITE;
+			break;
+		}
+	}
+
+	cwp->log_pos_white_last = log_lseek_get(cwp);
+	cwp->log_pos_ask_error = cwp->log_pos_white_last;
+}
+
+
+
+/* ask a DCC server */
+int					/* <0=big problem, 0=retryable, 1=ok */
+cmn_ask_dcc(CMN_WORK *cwp)
+{
+	int i;
+
+	/* Talk to the DCC server and make the X-DCC header.
+	 * If we have blacklist entries for it, then we'll tell the DCC
+	 * server it is spam and say so in the X-DCC header.
+	 * Note that a target count of 0 is a query. */
+	if (cwp->ask_st & ASK_ST_QUERY) {
+		cwp->cmn_fgs &= ~CMN_FG_LOCAL_SPAM;
+		cwp->local_tgts = 0;
+	} else if (cwp->ask_st & ASK_ST_CLNT_ISSPAM) {
+		cwp->cmn_fgs |= CMN_FG_LOCAL_SPAM;
+		cwp->local_tgts = cwp->tgts + cwp->mta_rej_tgts;
+	} else if (cwp->ask_st & ASK_ST_GREY_EMBARGO) {
+		/* if the message is under a greylist embargo,
+		 * then report to the DCC only the targets for
+		 * which it is an initial transmission or embargo #1 */
+		cwp->cmn_fgs &= ~CMN_FG_LOCAL_SPAM;
+		cwp->local_tgts = cwp->early_grey_tgts + cwp->mta_rej_tgts;
+	} else {
+		/* if this is the end of a greylist embargo
+		 * then do not tell the DCC about targets that were
+		 * counted with previous transmissions.  Those targets
+		 * are counted in late_grey_tgts this time, but were
+		 * counted in early_grey_tgts for previous transmissions */
+		cwp->cmn_fgs &= ~CMN_FG_LOCAL_SPAM;
+		cwp->local_tgts = (cwp->tgts - cwp->late_grey_tgts
+				   + cwp->mta_rej_tgts);
+	}
+
+	/* talk to the DCC server */
+	i = ask_dcc(cwp->emsg, cwp->dcc_ctxt, try_extra_hard,
+		    &cwp->header, &cwp->cks, &cwp->ask_st,
+		    (cwp->cmn_fgs & CMN_FG_LOCAL_SPAM) != 0,
+		    cwp->local_tgts);
+	if (i <= 0) {
+		cwp->log_pos_ask_error += thr_error_msg(cwp, "%s", cwp->emsg);
+		return i;
+	}
+
+	/* if we are talking to a new server,
+	 * remember to fix the X-DCC headers of the other contexts */
+	if (cwp->xhdr_fname_len != cwp->header.start_len
+	    || strncmp(cwp->header.buf, cwp->xhdr_fname, cwp->xhdr_fname_len)) {
+		if (dcc_clnt_debug > 1)
+			thr_trace_msg(cwp, DCC_XHDR_START
+				      "header changed from %s to %.*s",
+				      cwp->xhdr_fname,
+				      (int)cwp->header.start_len,
+				      cwp->header.buf);
+		cwp->xhdr_fname_len = get_xhdr_fname(cwp->xhdr_fname,
+						     sizeof(cwp->xhdr_fname),
+						     dcc_clnt_info);
+		if (++dcc_ctxt_sn == 0)
+			dcc_ctxt_sn = 1;
+		cwp->dcc_ctxt_sn = dcc_ctxt_sn;
+	}
+
+	return 1;
+}
+
+
+#define USER_LOG_CAPTION(rcpt_st, s) user_log_write((rcpt_st), (s), LITZ(s))
+#define USER_LOG_EOL(rcpt_st) USER_LOG_CAPTION((rcpt_st), "\n")
+
+static u_char
+user_log_write(RCPT_ST *rcpt_st, const void *buf, u_int len)
+{
+	DCC_PATH abs_nm;
+	int result;
+
+	if (user_log_fd < 0)
+		return 0;
+
+	if (!len)
+		len = strlen(buf);
+	result = write(user_log_fd, buf, len);
+	if (result == (int)len)
+		return 1;
+
+	if (result < 0)
+		thr_error_msg(rcpt_st->cwp, "write(%s): %s",
+			      fnm2abs_err(abs_nm, rcpt_st->user_log_nm),
+			      ERROR_STR());
+	else
+		thr_error_msg(rcpt_st->cwp,
+			      "write(%s)=%d instead of %d",
+			      fnm2abs_err(abs_nm, rcpt_st->user_log_nm),
+			      result, (int)len);
+	dcc_log_close(0, rcpt_st->user_log_nm, user_log_fd,
+		      &rcpt_st->cwp->ldate);
+	user_log_fd = -1;
+	return 0;
+}
+
+
+
+static void PATTRIB(2,3)
+user_log_print(RCPT_ST *rcpt_st, const char *p, ...)
+{
+	char logbuf[LOGBUF_SIZE*2];
+	int i;
+	va_list args;
+
+	if (user_log_fd < 0)
+		return;
+
+	va_start(args, p);
+	i = vsnprintf(logbuf, sizeof(logbuf), p, args);
+	va_end(args);
+	if (i < ISZ(logbuf)) {
+		user_log_write(rcpt_st, logbuf, i);
+		return;
+	}
+	user_log_write(rcpt_st, logbuf, sizeof(logbuf));
+	user_log_write(rcpt_st, "...", 3);
+}
+
+
+
+static void
+user_log_block(RCPT_ST *rcpt_st,	/* copy from main log file to this */
+	       off_t start,		/* starting here */
+	       off_t stop)		/* and ending here */
+{
+	CMN_WORK *cwp;
+	char buf[4096];
+	int len;
+	int result;
+
+	if (user_log_fd < 0)
+		return;
+
+	if (start == stop)
+		return;
+
+	cwp = rcpt_st->cwp;
+
+	if (start == -1 || stop == -1 || start > stop) {
+		thr_error_msg(cwp, "bogus user_log_block position %d to %d",
+			      (int)start, (int)stop);
+		return;
+	}
+
+	if (!log_lseek_set(cwp, start))
+		return;
+
+	while ((len = stop - start) != 0) {
+		if (len > ISZ(buf))
+			len = ISZ(buf);
+		result = read(log_fd2, buf, len);
+		if (result != len) {
+			if (result < 0)
+				thr_error_msg(cwp,
+					      "user_log_block read(%s,%d): %s",
+					      cwp->log_nm, len, ERROR_STR());
+			else
+				thr_error_msg(cwp, "user_log_block"
+					      " read(%s,%d)=%d",
+					      cwp->log_nm, len, result);
+			log_fd2_close(-2);
+			return;
+		}
+		if (!user_log_write(rcpt_st, buf, len))
+			return;
+		start += len;
+	}
+}
+
+
+/* print
+ *	      env_To: user@example.com			    ok
+ *  user@example.com: f7a48ff4 70d29d39 4ed1e36f 104e4fa0
+ *		      86e0517b b455b130 4cfccc8c f1a1ff37 Pass
+ */
+static void
+print_addr_sum(LOG_WRITE_FNC out, void *arg,
+	       const char *addr,
+	       int addr_len,		/* trim trailing '>' from grey addr */
+	       const char *sum,
+	       int sum_len,		/* trim trailing '>' from env_To */
+	       const char *tgts,
+	       int tgts_width)		/* to right-justify env_To tgts */
+{
+	char buf[100];
+	int i;
+
+	i = snprintf(buf, sizeof(buf), PRINT_CK_PAT_LIM_CK" %*s\n",
+		     addr_len, addr,
+		     addr_len > 0 ? ':' : ' ',
+		     sum_len, sum,
+		     tgts_width, tgts);
+	if (i >= ISZ(buf)) {
+		i = sizeof(buf);
+		buf[i-1] = '\n';
+	}
+	out(arg, buf, i);
+}
+
+
+
+static void
+print_env_to(LOG_WRITE_FNC out, void *arg, const RCPT_ST *rcpt_st)
+{
+	const char *addr;
+	int addr_len;
+	char tgts_buf[16];
+
+	if (rcpt_st->env_to_tgts != 0) {
+		addr = rcpt_st->env_to;
+		addr_len = strlen(addr);
+		if (addr_len > 1 && addr[0] == '<' && addr[addr_len-1] == '>') {
+			++addr;
+			addr_len -= 2;
+		}
+		print_addr_sum(out, arg,
+			       DCC_XHDR_TYPE_ENV_TO, LITZ(DCC_XHDR_TYPE_ENV_TO),
+			       addr, addr_len,
+			       dcc_tgts2str(tgts_buf, sizeof(tgts_buf),
+					    rcpt_st->env_to_tgts, 0),
+			       PRINT_CK_PAT_SRVR_LEN+PRINT_CK_PAT_WLIST_LEN);
+	}
+
+	if (rcpt_st->user_tgts != 0) {
+		addr = rcpt_st->user;
+		addr_len = strlen(addr);
+		if (addr_len > 1 && addr[0] == '<' && addr[addr_len-1] == '>') {
+			++addr;
+			addr_len -= 2;
+		}
+		print_addr_sum(out, arg,
+			       DCC_XHDR_TYPE_ENV_TO, LITZ(DCC_XHDR_TYPE_ENV_TO),
+			       addr, addr_len,
+			       dcc_tgts2str(tgts_buf, sizeof(tgts_buf),
+					    rcpt_st->user_tgts, 0),
+			       PRINT_CK_PAT_SRVR_LEN+PRINT_CK_PAT_WLIST_LEN);
+	}
+}
+
+
+
+static void
+print_grey(LOG_WRITE_FNC out, void *arg,
+	   const RCPT_ST *rcpt_st, u_char *headed)
+{
+#define CK_HEADING	    "       "DCC_XHDR_GREY_RECIP"\n"
+#define CK_HEADING_QUERY    "       "DCC_XHDR_GREY_RECIP" query\n"
+	char cbuf[DCC_CK2STR_LEN];
+	char embargo_buf[20];
+	const char *addr;
+	int addr_len;
+	const char *embargo;
+
+	embargo = 0;
+	switch (rcpt_st->grey_result) {
+	case ASK_GREY_FAIL:
+		embargo = DCC_XHDR_EMBARGO_FAIL;
+		break;
+	case ASK_GREY_OFF:
+		return;
+	case ASK_GREY_EMBARGO:
+		if (rcpt_st->embargo_num > 0) {
+			snprintf(embargo_buf, sizeof(embargo_buf),
+				 DCC_XHDR_EMBARGO_NUM, rcpt_st->embargo_num);
+			embargo = embargo_buf;
+		} else {
+			embargo = DCC_XHDR_EMBARGO;
+		}
+		break;
+	case ASK_GREY_EMBARGO_END:
+		embargo = DCC_XHDR_EMBARGO_ENDED;
+		break;
+	case ASK_GREY_PASS:
+		embargo = DCC_XHDR_EMBARGO_PASS;
+		break;
+	case ASK_GREY_WHITE:
+		embargo = DCC_XHDR_EMBARGO_OK;
+		break;
+	case ASK_GREY_SPAM:
+		snprintf(embargo_buf, sizeof(embargo_buf),
+			 DCC_XHDR_EMBARGO_RESET, rcpt_st->embargo_num);
+		embargo = embargo_buf;
+		break;
+	}
+
+	if (!headed || !*headed) {
+		if (headed)
+			*headed = 1;
+		if (rcpt_st->cwp->ask_st & ASK_ST_QUERY)
+			out(arg, CK_HEADING_QUERY, LITZ(CK_HEADING_QUERY));
+		else
+			out(arg, CK_HEADING, LITZ(CK_HEADING));
+	}
+
+	addr = rcpt_st->env_to;
+	addr_len = strlen(addr);
+	if (addr_len > 1 && addr[0] == '<' && addr[addr_len-1] == '>') {
+		++addr;
+		addr_len -= 2;
+	}
+	print_addr_sum(out, arg, addr, addr_len,
+		       dcc_ck2str(cbuf, sizeof(cbuf),
+				  DCC_CK_GREY_MSG, rcpt_st->msg_sum, 0),
+		       PRINT_CK_SUM_LEN,
+		       "", 0);
+	print_addr_sum(out, arg, "", 0,
+		       dcc_ck2str(cbuf, sizeof(cbuf),
+				  DCC_CK_GREY3, rcpt_st->triple_sum, 0),
+		       PRINT_CK_SUM_LEN,
+		       embargo, 0);
+
+	if (!headed)
+		out(arg, "\n", 1);
+
+#undef CK_HEADING
+}
+
+
+
+/* log external header, X-DCC header, and DCC results */
+static void
+log_isspam(LOG_WRITE_FNC fnc, void *cp, CMN_WORK *cwp,
+	   u_char log_type,		/* 0="" 1="per-user" 2="global" */
+	   FLTR_SWS sws, RCPT_FGS rcpt_fgs)
+{
+	ASK_ST ask_st;
+
+	ask_st = cwp->ask_st;
+	if (rcpt_fgs & RCPT_FG_WLIST_NOTSPAM)
+		ask_st |= ASK_ST_WLIST_NOTSPAM;
+	if (rcpt_fgs & RCPT_FG_WLIST_ISSPAM)
+		ask_st |= ASK_ST_WLIST_ISSPAM;
+	log_ask_st(fnc, cp, ask_st, sws, log_type, &cwp->header);
+}
+
+
+
+static void
+user_log_msg(CMN_WORK *cwp, RCPT_ST *rcpt_st)
+{
+	DCC_PATH rcpt_logdir;
+	const char *old_log;
+
+	/* since the user wants a log file, make one for the system */
+	cwp->ask_st |= ASK_ST_LOGIT;
+
+	/* we need the main log file to create per-user log files */
+	if (rcpt_st->dir[0] == '\0'
+	    || dcc_main_logdir[0] == '\0')
+		return;
+
+	if (!log2_start(cwp))
+		return;
+
+	/* create the user's log file */
+	snprintf(rcpt_logdir, sizeof(rcpt_logdir), "%s/log", rcpt_st->dir);
+	/* try to use the same name as the main log file */
+	old_log = rindex(cwp->log_nm, '.');
+	if (old_log) {
+		if (strlen(++old_log) != DCC_MKSTEMP_LEN)
+			old_log = 0;
+	}
+	user_log_fd = dcc_log_open(cwp->emsg, rcpt_st->user_log_nm,
+				   cwp->id, sizeof(cwp->id), old_log,
+				   rcpt_logdir, DCC_FIN_LOG_PREFIX,
+				   (rcpt_st->sws & FLTR_SW_LOG_M)
+				   ? LOG_MODE_MINUTE
+				   : (rcpt_st->sws & FLTR_SW_LOG_H)
+				   ? LOG_MODE_HOUR
+				   : (rcpt_st->sws & FLTR_SW_LOG_D)
+				   ? LOG_MODE_DAY
+				   : LOG_MODE_FLAT);
+	if (user_log_fd < 0) {
+		if (user_log_fd == -1)
+			thr_error_msg(cwp, "%s", cwp->emsg);
+		return;
+	}
+
+	user_log_block(rcpt_st,		/* copy envelope before env_To line */
+		       0, cwp->log_pos_to_first);
+	user_log_block(rcpt_st,		/* copy this env_To line */
+		       rcpt_st->log_pos_to,
+		       rcpt_st->fwd
+		       ? rcpt_st->fwd->log_pos_to
+		       : cwp->log_pos_to_end);
+	user_log_block(rcpt_st,		/* copy the body of the message */
+		       cwp->log_pos_to_end,
+		       cwp->log_pos_white_first);
+	user_log_block(rcpt_st,		/* copy whitelist error messages */
+		       rcpt_st->log_pos_white,
+		       rcpt_st->fwd
+		       ? rcpt_st->fwd->log_pos_white
+		       : cwp->log_pos_white_last);
+	user_log_block(rcpt_st,		/* copy DCC error messages */
+		       cwp->log_pos_white_last,
+		       cwp->log_pos_ask_error);
+
+	/* log external header, X-DCC header, and DCC results */
+	log_isspam((LOG_WRITE_FNC)user_log_write, rcpt_st, cwp, 1,
+		   rcpt_st->sws, rcpt_st->fgs);
+
+	/* log the checksums and their counts */
+	dcc_print_cks((LOG_WRITE_FNC)user_log_write, rcpt_st,
+		      cwp->cmn_fgs & CMN_FG_LOCAL_SPAM, cwp->local_tgts,
+		      &cwp->cks, rcpt_st->wtgts,
+		      (cwp->cmn_fgs & CMN_FG_LOG_ENV_TO) != 0);
+	print_env_to((LOG_WRITE_FNC)user_log_write, rcpt_st, rcpt_st);
+	USER_LOG_EOL(rcpt_st);
+	print_grey((LOG_WRITE_FNC)user_log_write, rcpt_st, rcpt_st, 0);
+}
+
+
+
+void
+log_smtp_reply(CMN_WORK *cwp)
+{
+	thr_log_print(cwp, 1, DCC_XHDR_REJ_DATA_MSG);
+	log_write(cwp, cwp->reply.rcode, 0);
+	LOG_CMN_CAPTION(cwp, " ");
+	log_write(cwp, cwp->reply.xcode, 0);
+	LOG_CMN_CAPTION(cwp, " ");
+	log_write(cwp, cwp->reply.str, 0);
+	LOG_CMN_EOL(cwp);
+}
+
+
+
+static void
+user_log_smtp_reply(CMN_WORK *cwp, RCPT_ST *rcpt_st)
+{
+	user_log_print(rcpt_st, DCC_XHDR_REJ_DATA_MSG"%s %s %s\n",
+		       cwp->reply.rcode, cwp->reply.xcode, cwp->reply.str);
+}
+
+
+
+/* tell the grey list to restore the embargo on a triple */
+static void
+grey_spam(CMN_WORK *cwp, RCPT_ST *rcpt_st)
+{
+	DCC_GREY_SPAM gs;
+	DCC_OP_RESP resp;
+
+	switch (rcpt_st->grey_result) {
+	case ASK_GREY_FAIL:
+	case ASK_GREY_OFF:
+	case ASK_GREY_WHITE:
+		return;
+	case ASK_GREY_EMBARGO:
+		if (rcpt_st->embargo_num == 0) {
+			rcpt_st->grey_result = ASK_GREY_OFF;
+			return;
+		}
+		break;
+	case ASK_GREY_EMBARGO_END:
+	case ASK_GREY_PASS:
+		break;
+	case ASK_GREY_SPAM:
+		dcc_logbad(EX_SOFTWARE, "cmn_ask_white grey_result=%d",
+			   rcpt_st->grey_result);
+		return;
+	}
+
+	memset(&gs, 0, sizeof(gs));
+
+	if (cwp->cks.sums[DCC_CK_IP].type != DCC_CK_IP) {
+		thr_error_msg(cwp, "missing IP checksum for dcc_grey_spam()");
+		return;
+	}
+
+	gs.ip.type = DCC_CK_IP;
+	gs.ip.len = sizeof(gs.ip);
+	memcpy(&gs.ip.sum, &cwp->cks.sums[DCC_CK_IP].sum, sizeof(DCC_SUM));
+
+	gs.triple.type = DCC_CK_GREY3;
+	gs.triple.len = sizeof(gs.triple);
+	memcpy(&gs.triple.sum, rcpt_st->triple_sum, sizeof(DCC_SUM));
+
+	gs.msg.type = DCC_CK_GREY_MSG;
+	gs.msg.len = sizeof(gs.msg);
+	memcpy(&gs.msg.sum, rcpt_st->msg_sum, sizeof(DCC_SUM));
+
+	if (!dcc_clnt_op(cwp->emsg, cwp->dcc_ctxt, DCC_CLNT_FG_GREY,
+			   0, 0, 0, &gs.hdr, sizeof(gs),
+			   DCC_OP_GREY_SPAM, &resp, sizeof(resp))) {
+		thr_error_msg(cwp, "%s", cwp->emsg);
+	}
+
+	rcpt_st->grey_result = ASK_GREY_SPAM;
+}
+
+
+
+/* process the message for each user to decide what to do with it */
+void
+users_process(CMN_WORK *cwp)
+{
+	RCPT_ST *rcpt_st;
+	u_char need_eol;
+	int trap_acc_tgts;
+	DNSBL_GBITS dnsbl_sws, dnsbl_hits, common_dnsbl_hits;
+	int dnsbl_delay_tgts;
+	u_char dnsbl_timeo;
+	const DNSBL_GROUP *blg;
+	const REPLY_TPLT *reply;
+	int i;
+
+	/* log the DCC results and headers in the common log file */
+	log_isspam((LOG_WRITE_FNC)log_write, cwp, cwp, 2,
+		   cwp->rcpts_sws, cwp->rcpt_fgs);
+
+	/* log the checksums, DCC server counts and global whitelist values */
+	dcc_print_cks((LOG_WRITE_FNC)log_write, cwp,
+		      cwp->cmn_fgs & CMN_FG_LOCAL_SPAM, cwp->local_tgts,
+		      &cwp->cks, cwp->wtgts,
+		      (cwp->cmn_fgs & CMN_FG_LOG_ENV_TO) != 0);
+	if (cwp->cmn_fgs & CMN_FG_LOG_ENV_TO) {
+		for (rcpt_st = cwp->rcpt_st_first;
+		     rcpt_st;
+		     rcpt_st = rcpt_st->fwd) {
+			print_env_to((LOG_WRITE_FNC)log_write, cwp, rcpt_st);
+		}
+	}
+	LOG_CMN_EOL(cwp);
+
+	/* mark recipients who won't receive it */
+	need_eol = 0;
+	trap_acc_tgts = 0;
+	common_dnsbl_hits = -1;
+	dnsbl_delay_tgts = 0;
+	for (rcpt_st = cwp->rcpt_st_first;
+	     rcpt_st;
+	     rcpt_st = rcpt_st->fwd) {
+		/* Ignore recipients whose RCPT_TO commands were rejected */
+		if (rcpt_st->fgs & RCPT_FG_REJ_FILTER)
+			continue;
+
+		/* We cannot decide whether the message might be spam
+		 * for targets rejected by the MTA because dccm does not
+		 * have the mailer and so cannot find the likely
+		 * userdirs/local/user/whiteclnt file */
+		if (rcpt_st->fgs & RCPT_FG_BAD_USERNAME)
+			continue;
+
+		if (rcpt_st->fgs & RCPT_FG_WHITE) {
+			common_dnsbl_hits = 0;
+			dnsbl_timeo = 0;
+		} else {
+			/* decide whether it is spam for this target */
+			if ((rcpt_st->fgs & RCPT_FG_BLACK)
+			    || ((cwp->ask_st & ASK_ST_SRVR_ISSPAM)
+				&& !(rcpt_st->sws & FLTR_SW_DCC_OFF)))
+				rcpt_st->fgs |= RCPT_FG_ISSPAM;
+
+			dnsbl_sws = FLTR_SW_DNSBL_BITS(rcpt_st->sws);
+			dnsbl_hits = (dnsbl_sws
+				      & ASK_ST_DNSBL_HIT_BITS(cwp->ask_st));
+			common_dnsbl_hits &= dnsbl_hits;
+			if (dnsbl_hits != 0) {
+				dnsbl_timeo = 0;
+				rcpt_st->fgs |= RCPT_FG_ISSPAM;
+			} else if (0 != (ASK_ST_DNSBL_TFAIL_BITS(cwp->ask_st)
+					 & dnsbl_sws)) {
+				dnsbl_timeo = 1;
+			} else {
+				dnsbl_timeo = 0;
+			}
+		}
+
+		if (rcpt_st->fgs & RCPT_FG_ISSPAM) {
+			if (rcpt_st->sws & FLTR_SW_TRAP_ACC)
+				++trap_acc_tgts;
+			else
+				++cwp->reject_tgts;
+		} else if (dnsbl_timeo) {
+			++dnsbl_delay_tgts;
+		} else {
+			++cwp->deliver_tgts;
+		}
+
+		/* Tell greylist to restore the embargo for targets that believe
+		 * the message was spam and did not white- or blacklist it */
+		if ((rcpt_st->fgs & RCPT_FG_ISSPAM)
+		    && !(rcpt_st->sws & FLTR_SW_TRAPS))
+			grey_spam(cwp, rcpt_st);
+
+		print_grey((LOG_WRITE_FNC)log_write, cwp, rcpt_st, &need_eol);
+	}
+	if (need_eol)
+		LOG_CMN_EOL(cwp);
+
+	/* If real targets or rejection-traps need to reject the message,
+	 * then treat any accept-traps as reject-traps to avoid telling the
+	 * SMTP client that the message was accepted. */
+	if (cwp->reject_tgts != 0 && cwp->deliver_tgts == 0) {
+		cwp->reject_tgts += trap_acc_tgts;
+	} else {
+		cwp->deliver_tgts += trap_acc_tgts;
+	}
+
+	/* temp-fail ambiguous mail if DNSBL checks timed out */
+	if (dnsbl_delay_tgts != 0) {
+		if (cwp->deliver_tgts != 0) {
+			/* It is not ambiguous if it must be delivered to
+			 * at least one recipient. */
+			cwp->deliver_tgts += dnsbl_delay_tgts;
+			dnsbl_delay_tgts = 0;
+		} else {
+			cwp->reject_tgts += dnsbl_delay_tgts;
+		}
+	}
+
+	if (cwp->reject_tgts != 0 && cwp->deliver_tgts != 0) {
+		/* It is spam for some targets and not for others.
+		 * If we cannot discard, then reject it for all targets */
+		if ((cwp->cmn_fgs & CMN_FG_FROM_SUBMIT)
+		    || cannot_discard) {
+			thr_log_print(cwp, 0,
+				      "rejection forced for %d targets\n",
+				      cwp->deliver_tgts);
+			cwp->reject_tgts += cwp->deliver_tgts;
+			cwp->deliver_tgts = 0;
+		} else {
+			thr_log_print(cwp, 0,
+				      "discard forced for %d targets\n",
+				      cwp->reject_tgts);
+		}
+	}
+
+	/* do not embargo the message if no target wants it */
+	if (cwp->deliver_tgts == 0)
+		cwp->ask_st &= ~ASK_ST_GREY_EMBARGO;
+
+	if (cwp->ask_st & ASK_ST_GREY_EMBARGO) {
+		make_reply(&cwp->reply, &grey_reply, cwp, 0);
+		return;
+	}
+
+	/*  finished if it is not spam or we won't do anything about it */
+	if (cwp->reject_tgts == 0 || cwp->action == CMN_IGNORE)
+		return;
+
+	/* make an SMTP rejection message unless we have already have one */
+	if (cwp->reply.log_result)
+		return;
+
+	/* if possible, use a DNSBL rejection message that applies
+	 * to all recipients */
+	if (common_dnsbl_hits != 0) {
+		for (i = 0; i < MAX_DNSBL_GROUPS; ++i) {
+			if ((common_dnsbl_hits & DNSBL_G2B(i)) != 0
+			    && 0 != (blg = &cwp->cks.dnsbl->groups[i])
+			    && 0 != (reply = blg->dnsbl->reply)) {
+				make_reply(&cwp->reply, reply, cwp, blg);
+				return;
+			}
+		}
+	}
+
+	if (dnsbl_delay_tgts != 0) {
+		make_reply(&cwp->reply, &dnsbl_timeo_reply, cwp, 0);
+		return;
+	}
+
+	/* use the generic DCC rejection message */
+	make_reply(&cwp->reply, &reject_reply, cwp, 0);
+}
+
+
+
+/* after having checked each user or recipient,
+ *	dispose of the message for each */
+static void
+user_log_result(CMN_WORK *cwp, RCPT_ST *rcpt_st, const char *result)
+{
+	/* create the per-user log file */
+	if ((rcpt_st->fgs & RCPT_FG_ISSPAM)
+	    || ((cwp->ask_st & ASK_ST_GREY_LOGIT)
+		&& !(rcpt_st->sws & FLTR_SW_GREY_LOG_OFF))
+	    || (rcpt_st->sws & FLTR_SW_LOG_ALL))
+		user_log_msg(cwp, rcpt_st);
+
+	if (cwp->ask_st & ASK_ST_INVALID_MSG) {
+		user_log_print(rcpt_st, DCC_XHDR_RESULT"%s\n", result);
+		return;
+	}
+
+	if (rcpt_st->rej_msg[0] != '\0') {
+		/* rejection result for this recipient in the main log */
+		thr_log_print(cwp, 0,
+			      DCC_XHDR_RESULT_REJECT" %s: %s\n",
+			      rcpt_st->env_to, rcpt_st->rej_result);
+		/* per-user log file */
+		if (!(rcpt_st->fgs & RCPT_FG_INCOMPAT_REJ)
+		    || (rcpt_st->sws & FLTR_SW_LOG_ALL)) {
+			USER_LOG_CAPTION(rcpt_st, DCC_XHDR_REJ_RCPT_MSG);
+			user_log_write(rcpt_st, rcpt_st->rej_msg, 0);
+			if (rcpt_st->rej_msg[0] == '4')
+				USER_LOG_CAPTION(rcpt_st,
+						 "\n"DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_REJECT
+						 " temporarily\n");
+			else
+				USER_LOG_CAPTION(rcpt_st,
+						 "\n"DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_REJECT);
+		}
+		return;
+	}
+
+	if (cwp->ask_st & ASK_ST_GREY_EMBARGO) {
+		user_log_smtp_reply(cwp, rcpt_st);
+		if (rcpt_st->embargo_num != 0) {
+			user_log_print(rcpt_st,
+				       DCC_XHDR_RESULT"%s #%d\n",
+				       cwp->reply.log_result,
+				       rcpt_st->embargo_num);
+		} else {
+			user_log_print(rcpt_st, DCC_XHDR_RESULT"%s\n",
+				       cwp->reply.log_result);
+		}
+		return;
+	}
+
+	if (!(rcpt_st->fgs & RCPT_FG_ISSPAM)) {
+		/* It is not spam for this target.
+		 *
+		 * If it was rejected late, e.g. by the SMTP server for dccifd
+		 * in proxy, mode, then log the rejection message */
+		if (result) {
+			if (cwp->reply.str && cwp->reply.str[0] != '\0')
+				user_log_print(rcpt_st,
+					       DCC_XHDR_REJ_DATA_MSG"%s\n",
+					       cwp->reply.str);
+			user_log_print(rcpt_st, DCC_XHDR_RESULT"%s\n",
+				       result);
+			return;
+		}
+
+		/* If it was spam for some other target and we cannot
+		 * discard for that target, then log the rejection */
+		if (cwp->reject_tgts != 0
+		    && ((cwp->cmn_fgs & CMN_FG_FROM_SUBMIT)
+			|| cannot_discard)) {
+			user_log_smtp_reply(cwp, rcpt_st);
+			USER_LOG_CAPTION(rcpt_st,
+					 DCC_XHDR_RESULT
+					 DCC_XHDR_RESULT_REJECT
+					 DCC_XHDR_RESULT_FORCED"\n");
+			return;
+		}
+
+		if (cwp->ask_st & (ASK_ST_CLNT_ISSPAM
+				   | ASK_ST_SRVR_ISSPAM
+				   | ASK_ST_REP_ISSPAM)) {
+			if (cwp->rcpt_fgs & RCPT_FG_GREY_END)
+				USER_LOG_CAPTION(rcpt_st,
+						 DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_I_A
+						 " "DCC_XHDR_RESULT_A_GREY"\n");
+			else
+				USER_LOG_CAPTION(rcpt_st,
+						 DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_I_A"\n");
+			return;
+		}
+		if (cwp->rcpt_fgs & RCPT_FG_GREY_END) {
+			USER_LOG_CAPTION(rcpt_st,
+					 DCC_XHDR_RESULT
+					 DCC_XHDR_RESULT_ACCEPT
+					 " "DCC_XHDR_RESULT_A_GREY"\n");
+			return;
+		}
+		if (rcpt_st->fgs & RCPT_FG_GREY_WHITE) {
+			USER_LOG_CAPTION(rcpt_st,
+					 DCC_XHDR_RESULT
+					 DCC_XHDR_RESULT_ACCEPT
+					 ";  greylist whitelist\n");
+			return;
+		}
+		USER_LOG_CAPTION(rcpt_st,
+				 DCC_XHDR_RESULT DCC_XHDR_RESULT_ACCEPT"\n");
+		return;
+	}
+
+	/* It was spam for this target
+	 *
+	 * If some other target wanted the message, then we should discard it
+	 * for this target.  Dccifd in proxy mode cannot discard for an
+	 * individual target, and so must avoid the possibility by
+	 * recipients that might have differing choices with cannot_discard. */
+	if (cwp->deliver_tgts != 0) {
+		/* Prefer to discard for spam traps, but just accept
+		 * spam when we cannot discard it for dccifd */
+		if (rcpt_st->sws & FLTR_SW_TRAPS) {
+			if (!cannot_discard) {
+				user_reject_discard(cwp, rcpt_st);
+				USER_LOG_CAPTION(rcpt_st, DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_DISCARD"\n");
+			} else {
+				USER_LOG_CAPTION(rcpt_st, DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_ACCEPT"\n");
+			}
+
+		} else {
+			--cwp->reject_tgts;
+			++totals.tgts_discarded;
+			user_reject_discard(cwp, rcpt_st);
+			if (cwp->action == CMN_DISCARD) {
+				USER_LOG_CAPTION(rcpt_st, DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_DISCARD"\n");
+			} else {
+				thr_log_print(cwp, 0,
+					      DCC_XHDR_RESULT
+					      DCC_XHDR_RESULT_DISCARD
+					      " forced for %s\n",
+					      rcpt_st->env_to);
+				USER_LOG_CAPTION(rcpt_st,
+						 DCC_XHDR_RESULT
+						 DCC_XHDR_RESULT_DISCARD
+						 DCC_XHDR_RESULT_FORCED"\n");
+			}
+		}
+		return;
+	}
+
+	/* spam for all targets including this one */
+
+	if (cwp->action == CMN_DISCARD) {
+		USER_LOG_CAPTION(rcpt_st, DCC_XHDR_RESULT
+				 DCC_XHDR_RESULT_DISCARD"\n");
+		return;
+	}
+
+	if (cwp->action == CMN_IGNORE) {
+		USER_LOG_CAPTION(rcpt_st, DCC_XHDR_RESULT
+				 DCC_XHDR_RESULT_I_A"\n");
+		return;
+	}
+
+	user_log_smtp_reply(cwp, rcpt_st);
+	if (rcpt_st->sws & FLTR_SW_TRAP_ACC) {
+		user_log_print(rcpt_st, DCC_XHDR_RESULT
+			       "%s"DCC_XHDR_RESULT_FORCED"\n",
+			       cwp->reply.log_result);
+	} else {
+		user_log_print(rcpt_st, DCC_XHDR_RESULT"%s\n",
+			       cwp->reply.log_result);
+	}
+}
+
+
+
+/* log the consensus in each target's log file */
+void
+users_log_result(CMN_WORK *cwp,
+		 const char *result)	/* 0 or reject by dccifd's server */
+{
+	RCPT_ST *rcpt_st;
+	int error;
+
+	error = pthread_mutex_lock(&user_log_mutex);
+	if (error)
+		dcc_logbad(EX_SOFTWARE, "pthread_mutex_lock(user_log): %s",
+			   ERROR_STR1(error));
+	user_log_owner = pthread_self();
+
+	/* create individual log files and trim target list */
+	for (rcpt_st = cwp->rcpt_st_first; rcpt_st; rcpt_st = rcpt_st->fwd) {
+		if (rcpt_st->fgs & RCPT_FG_BAD_USERNAME)
+			continue;
+		if ((cwp->ask_st & ASK_ST_INVALID_MSG)
+		    && !(rcpt_st->sws & FLTR_SW_LOG_ALL))
+			continue;
+
+		user_log_result(cwp, rcpt_st, result);
+
+		if (user_log_fd >= 0) {
+			dcc_log_close(0, rcpt_st->user_log_nm,
+				      user_log_fd, &cwp->ldate);
+			user_log_fd = -1;
+		}
+	}
+
+	log_fd2_close(-1);
+
+	user_log_owner = 0;
+	error = pthread_mutex_unlock(&user_log_mutex);
+	if (error)
+		dcc_logbad(EX_SOFTWARE, "pthread_mutex_unlock(user_log): %s",
+			   ERROR_STR1(error));
+}