view thrlib/cmn.c @ 6:c7785b85f2d2 default tip

Init scripts try to conform LSB header
author Peter Gervai <grin@grin.hu>
date Tue, 10 Mar 2009 15:15:36 +0100
parents c7f6b056b673
children
line wrap: on
line source

/* 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));
}