/* Distributed Checksum Clearinghouse
 *
 * ask about a batch of checksums
 *
 * --C-LICENSE--
 * $Revision: 1.166 $
 */

#include "dcc_ck.h"
#include "dcc_heap_debug.h"
#include "dcc_xhdr.h"

u_char spamassassin_body_kludge;

static CKSUM_THOLDS tholds_log;
CKSUM_THOLDS tholds_rej;
static u_char dcc_honor_nospam[DCC_DIM_CKS];

static u_char trim_grey_ip_addr;	/* remove IP address from grey triple */
static struct in6_addr grey_ip_mask;

static void honor_cnt(const GOT_CKS *cks, u_int *, DCC_CK_TYPES, DCC_TGTS);



#ifdef DCC_PKT_VERS5
/* figure old server's target count before our latest report */
static DCC_TGTS				/* return corrected current count */
save_p_tgts(GOT_SUM *g,			/* put previous count in g->tgts */
	    DCC_OPS op,
	    const u_char is_bulk,	/* computed with body counts */
	    const DCC_TGTS local_tgts,	/* real local target count */
	    const DCC_TGTS gross_tgts,	/* local count adjusted by blacklist */
	    DCC_TGTS c_tgts)		/* what the old DCC server said */
{
	DCC_CK_TYPES type = g->type;

	if (op == DCC_OP_QUERY) {
		/* if we switched servers and converted a report
		 * to a query, then guess the total that the
		 * server would have produced for a report
		 * instead of the query we sent.
		 *
		 * Assume the server is not running with -K.
		 * If the server's current value is 0 for a body checksum
		 * then assume the report we sent to the other server has not
		 * been flooded.
		 * Assume other checksums will always be zero/unknown. */
		if (DB_GLOBAL_NOKEEP(0, type))
			return 0;

		/* Assume the current value is really the previous value
		 * because flooding has not happened */
		g->tgts = c_tgts;

		if (c_tgts < DCC_TGTS_TOO_MANY
		    && (DCC_CK_IS_BODY(type)
			|| type == DCC_CK_REP_TOTAL
			|| (type == DCC_CK_REP_BULK && is_bulk))) {
			c_tgts += local_tgts;
			if (c_tgts > DCC_TGTS_TOO_MANY)
				c_tgts = DCC_TGTS_TOO_MANY;
		}
		return c_tgts;

	} else if (type == DCC_CK_REP_TOTAL) {
		if (c_tgts >= local_tgts)
			g->tgts = c_tgts - local_tgts;

	} else if (type == DCC_CK_REP_BULK) {
		if (!is_bulk)
			g->tgts = c_tgts;
		else if (c_tgts >= local_tgts)
			g->tgts = c_tgts - local_tgts;

	} else if (c_tgts >= gross_tgts
		   && gross_tgts < DCC_TGTS_TOO_MANY) {
		/* if possible infer server's value before our report */
		if (c_tgts >= DCC_TGTS_TOO_MANY)
			g->tgts = c_tgts;
		else
			g->tgts = c_tgts - gross_tgts;
	}

	return c_tgts;
}



#endif /* DCC_PKT_VERS5 */
int					/* 1=ok, 0=no answer, -1=fatal */
ask_dcc(DCC_EMSG *emsg,
	DCC_CLNT_CTXT *ctxt,
	DCC_CLNT_FGS clnt_fgs,		/* DCC_CLNT_FG_* */
	DCC_HEADER_BUF *hdr,		/* put results here */
	GOT_CKS *cks,			/*	and here */
	ASK_ST *ask_stp,		/*	and here */
	u_char spam,			/* spam==0 && local_tgts==0 --> query */
	u_char check_rep,		/* reputation makes spam for a target */
	DCC_TGTS total_tgts,		/* includes previous greylisted tgts */
	DCC_TGTS local_tgts)		/* report these targets to DCC server */
{
	union {
	    DCC_HDR	hdr;
	    DCC_REPORT	r;
	} rpt;
	DCC_OP_RESP resp;
	DCC_OPS op;
	DCC_CK *ck;
	GOT_SUM *g;
	DCC_TGTS gross_tgts;
	DCC_TGTS c_tgts;		/* server's current, total count */
	DCC_TGTS rep_total = 0;
	u_char is_bulk;
	u_char srvr_all_many;		/* all body counts are MANY */
	u_char srvr_one_many;		/* at least one MANY body count */
	CKS_WTGTS hdr_tgts;		/* values for X-DCC header */
	DCC_CK_TYPES type;
	DCC_SRVR_ID srvr_id;
	int pkt_len, recv_len, exp_len;
	int num_cks, ck_num, result;

	memset(&hdr_tgts, 0, sizeof(hdr_tgts));

	/* prepare a report for the nearest DCC server */
	if (local_tgts == 0 && !spam) {
		/* because of greylisting, we can have a target count of 0
		 * but need to report spam discovered by a DNS list */
		op = DCC_OP_QUERY;
		gross_tgts = 0;
		rpt.r.tgts = 0;
	} else {
		op = DCC_OP_REPORT;
		if (local_tgts == DCC_TGTS_TOO_MANY
		    || local_tgts == 0) {
			spam = 1;
			local_tgts = 1;
		}
		if (spam) {
			*ask_stp |= (ASK_ST_CLNT_ISSPAM | ASK_ST_LOGIT
				     | ASK_ST_RPT_SPAM);
			gross_tgts = DCC_TGTS_TOO_MANY;
			rpt.r.tgts = htonl(local_tgts | DCC_TGTS_SPAM);
		} else {
			gross_tgts = local_tgts;
			rpt.r.tgts = htonl(local_tgts);
		}
	}

	ck = rpt.r.cks;
	num_cks = 0;
	for (g = cks->sums; g <= &cks->sums[DCC_CK_TYPE_LAST]; ++g) {
		/* do not tell the DCC server about some checksums */
		if (!g->rpt2srvr)
			continue;
		ck->len = sizeof(*ck);
		ck->type = g->type;
		ck->sum = g->sum;
		++ck;
		++num_cks;
	}
	if (num_cks == 0) {
		/* pretend we always have at least a basic body checksum
		 * and guess that the DCC server would have answered 0 */
		xhdr_init(hdr, 0);
		xhdr_add_ck(hdr, DCC_CK_BODY, gross_tgts);
		honor_cnt(cks, ask_stp, DCC_CK_BODY, local_tgts);
		return 1;
	}

	/* send the report and see what the DCC server has to say */
	pkt_len = (sizeof(rpt.r) - sizeof(rpt.r.cks)
		   + num_cks * sizeof(rpt.r.cks[0]));
	result = dcc_clnt_op(emsg, ctxt, clnt_fgs, 0, &srvr_id, 0,
			     &rpt.hdr, pkt_len, op, &resp, sizeof(resp));

	/* try a query to different server if the first failed
	 * but a second was found */
	if (!result && srvr_id != DCC_ID_INVALID) {
		if (dcc_clnt_debug) {
			if (emsg && emsg->c[0] != '\0') {
				dcc_trace_msg("retry with different server"
					      " after: %s", emsg->c);
				emsg->c[0] = '\0';
			} else {
				dcc_trace_msg("retry with different server");
			}
		}
		op = DCC_OP_QUERY;
		result = dcc_clnt_op(emsg, ctxt, clnt_fgs | DCC_CLNT_FG_RETRY,
				     0, &srvr_id, 0,
				     &rpt.hdr, pkt_len,
				     op, &resp, sizeof(resp));
	}
	if (!result) {
		*ask_stp |= ASK_ST_LOGIT;
	} else {
		/* forget about it if the DCC server responded too strangely */
		recv_len = ntohs(resp.hdr.len);
#ifdef DCC_PKT_VERS5
		if (resp.hdr.pkt_vers <= DCC_PKT_VERS5)
			exp_len = (sizeof(resp.ans5) - sizeof(resp.ans5.b)
				   + num_cks*sizeof(DCC_TGTS));
		else
#endif
			exp_len = (sizeof(resp.ans) - sizeof(resp.ans.b)
				   + num_cks*sizeof(resp.ans.b[0]));
		if (recv_len != exp_len) {
			dcc_pemsg(EX_UNAVAILABLE, emsg,
				  "DCC %s: answered with %d instead of %d bytes",
				  dcc_srvr_nm(0), recv_len, exp_len);
			*ask_stp |= ASK_ST_LOGIT;
			result = -1;
		}
	}

	/* check the server's response to see if we have spam */
	is_bulk = (gross_tgts >= BULK_THRESHOLD);
	srvr_one_many = 0;
	srvr_all_many = 1;
	ck_num = 0;
	for (g = cks->sums; g <= &cks->sums[DCC_CK_TYPE_LAST]; ++g) {
		/* We did not have this checksum. */
		if (!g->rpt2srvr) {
			/* pretend we always have a basic body checksum */
			if (g == &cks->sums[DCC_CK_BODY])
				honor_cnt(cks, ask_stp,
					  DCC_CK_BODY, local_tgts);
			continue;
		}

		type = g->type;
		if (result <= 0) {
			c_tgts = (DCC_CK_IS_BODY(type)) ? gross_tgts : 0;

#ifdef DCC_PKT_VERS5
		} else if (resp.hdr.pkt_vers <= DCC_PKT_VERS5) {
			c_tgts = save_p_tgts(g, op,
					     is_bulk,
					     local_tgts, gross_tgts,
					     ntohl(resp.ans5.b[ck_num]));
		} else {
#endif /* DCC_PKT_VERS5 */
			/* server's total before our report */
			g->tgts = ntohl(resp.ans.b[ck_num].p);
			/* new total */
			c_tgts = ntohl(resp.ans.b[ck_num].c);
#ifdef DCC_PKT_VERS5
		}
#endif
		++ck_num;

		if (DCC_CK_IS_BODY(type)) {
			if (c_tgts >= BULK_THRESHOLD)
				is_bulk = 1;
			if (c_tgts < DCC_TGTS_TOO_MANY)
				srvr_all_many = 0;
			else
				srvr_one_many = 1;
		}

		/* only compute bulk ratio with enough data */
		if (type == DCC_CK_REP_TOTAL) {
			/* Never add to X-DCC header for IP address total.
			 * Total reputation tgts can cause logging but
			 * not rejection. */
			if (c_tgts >= tholds_log.t[DCC_CK_REP_TOTAL])
				*ask_stp |= ASK_ST_LOGIT;
			/* rejection threshold sets reputation significance */
			if (c_tgts >= cks->tholds_rej.t[DCC_CK_REP_TOTAL])
				rep_total = c_tgts;
			continue;
		}

		/* assume that DCC_CK_REP_BULK is the last checksum,
		 * and so all other checksums have been seen,
		 * and so rep_total and bulk are right */
		if (type == DCC_CK_REP_BULK) {
			if (rep_total != 0) {
				u_int rep = get_reputation(c_tgts, rep_total);
				hdr_tgts.v[type] = rep+1;

				if (cks->tholds_rej.t[DCC_CK_REP_BULK] <= rep
				    && !(*ask_stp & ASK_ST_SRVR_NOTSPAM)) {
					*ask_stp |= (ASK_ST_REP_ISSPAM
						     | ASK_ST_LOGIT);
				}

				if (tholds_log.t[DCC_CK_REP_BULK] <= rep)
					*ask_stp |= ASK_ST_LOGIT;
			}
			continue;
		}

		hdr_tgts.v[type] = c_tgts;

		/* notice DCC server's whitelist */
		if (dcc_honor_nospam[type]) {
			if (c_tgts == DCC_TGTS_OK) {
				*ask_stp |= ASK_ST_SRVR_NOTSPAM;

			} else if (c_tgts == DCC_TGTS_OK2) {
				/* if server says it is half ok,
				 * look for two halves */
				if (*ask_stp & ASK_ST_SRVR_OK2) {
					*ask_stp |= ASK_ST_SRVR_NOTSPAM;
				} else {
					*ask_stp |= ASK_ST_SRVR_OK2;
				}
			}
		}

		honor_cnt(cks, ask_stp, type, c_tgts);
	}

	/* honor server whitelist */
	if (*ask_stp & ASK_ST_SRVR_NOTSPAM)
		*ask_stp &= ~ASK_ST_SRVR_ISSPAM;

	/* Tell the DCC server if the message was not spam on its own counts
	 *	but the reputation of the sender made the message spam for
	 *	at least one recipient.
	 * If the message was spam, we still need to tell the DCC Reputation
	 *	server about targets that were counted during greylisting
	 *	before the embargo expired and not reported to the DCC server
	 *	this time.
	 */
	if ((!srvr_all_many && check_rep
	     && (*ask_stp & ASK_ST_REP_ISSPAM) && total_tgts != 0)
	    || (srvr_all_many && total_tgts > local_tgts)) {
		u_char send_rep, send_bodies;

		if (srvr_one_many)
			rpt.r.tgts = htonl(1 | DCC_TGTS_REP_SPAM);
		else
			rpt.r.tgts = htonl(1 | DCC_TGTS_REP_SPAM
					   | DCC_TGTS_SPAM);
		ck = rpt.r.cks;
		num_cks = 0;
		send_rep = 0;
		send_bodies = 0;
		for (g = cks->sums; g <= &cks->sums[DCC_CK_TYPE_LAST]; ++g) {
			if (!g->rpt2srvr)
				continue;
			if (g->type == DCC_CK_REP_TOTAL) {
				DCC_TGTS tgts;

				/* Try to make the database contents be the
				 * same as they would be if mail were delivered
				 * with one recipient per transaction and
				 * with the first recipient caring about
				 * reputations. */

				if (srvr_all_many) {
					/* Worsen sender's reputation with
					 * previously embargoed targets if
					 * the message is now spam.
					 * When it was embargoed, it was not
					 * spam or we would not have old
					 * embargo counts.
					 * However, it might have had counts
					 * above BULK_THRESHOLD and been bulk.
					 * This can over-report a spamming
					 * IP address, but let's hope rarely */
					tgts = total_tgts - local_tgts;

				} else if (!is_bulk) {
					/* If the message was addressed to >=2
					 * targets but was not known to be spam
					 * until now, then hit the IP address.
					 * Otherwise, there would be a
					 * difference between sending
					 * this message in one SMTP transaction
					 * or 2 or more transactions.
					 * Only later transactions would find
					 * the message marked bulk and would
					 * report the sender as a bulk mailer */
					tgts = total_tgts - 1;
				} else {
					continue;
				}
				if (tgts > 0)
					send_rep = 1;
				rpt.r.tgts = htonl(tgts
						   | DCC_TGTS_REP_SPAM
						   | DCC_TGTS_SPAM);
				/* Do not include this checksum and
				 * so increase the total reputation count
				 * with this extra DCC report, because all
				 * recipients have already been counted. */
				continue;
			} else if (g->type == DCC_CK_REP_BULK) {
				/* This checksum always comes after
				 * DCC_CK_REP_TOTAL.  One of the points of this
				 * exercise is tell the server about additional
				 * reasons for a reputation. */
				if (!send_rep)
					continue;
			} else if (DCC_CK_IS_BODY(g->type)) {
				/* Include the body checksums even if the server
				 * always knows they are spam to ease
				 * investigations of bulk mail reputations. */
				if (!srvr_all_many) {
					/* We need to send this report if server
					 * did not know the message is spam.
					 * Otherwise this checksum just tags
					 * along with the reputation report. */
					send_bodies = 1;
					/* Force X-DCC header counts to be "many"
					 * since that's what they are now. */
					hdr_tgts.v[g->type] = DCC_TGTS_TOO_MANY;
				}
			} else {
				continue;
			}
			ck->len = sizeof(*ck);
			ck->type = g->type;
			ck->sum = g->sum;
			++ck;
			++num_cks;
		}
		if ((send_rep || send_bodies) && result > 0) {
			pkt_len = (sizeof(rpt.r) - sizeof(rpt.r.cks)
				   + num_cks * sizeof(rpt.r.cks[0]));
			dcc_clnt_op(emsg, ctxt, clnt_fgs, 0, &srvr_id, 0,
				    &rpt.hdr, pkt_len, DCC_OP_REPORT,
				    &resp, sizeof(resp));
			*ask_stp |= ASK_ST_RPT_SPAM;
		}
	}

	/* generate the header line now that we have checked all of
	 * the counts against their thresholds and so know if we
	 * must add "bulk".  Add the header even if checking is turned off
	 * and we won't reject affected messages.  Say "many" for DNSBL
	 * or local blacklist spam even without an answer from the DCC server
	 * so that SpamAssassin gets the message. */
	xhdr_init(hdr, srvr_id);
	if (*ask_stp & ASK_ST_SRVR_ISSPAM) {
		xhdr_add_str(hdr, DCC_XHDR_BULK);
	} else if (*ask_stp & ASK_ST_CLNT_ISSPAM) {
		xhdr_add_str(hdr, DCC_XHDR_BULK);
		if (spamassassin_body_kludge)
			hdr_tgts.v[DCC_CK_BODY] = DCC_TGTS_TOO_MANY;
	} else if (*ask_stp & ASK_ST_REP_ISSPAM) {
		xhdr_add_str(hdr, DCC_XHDR_BULK_REP);
		if (spamassassin_body_kludge)
			hdr_tgts.v[DCC_CK_BODY] = DCC_TGTS_TOO_MANY;
	}

	for (g = cks->sums; g <= &cks->sums[DCC_CK_TYPE_LAST]; ++g) {
		if (!g->rpt2srvr) {
			/* pretend we always have a body checksum */
			if (g == &cks->sums[DCC_CK_BODY])
				xhdr_add_ck(hdr, DCC_CK_BODY,
					    hdr_tgts.v[DCC_CK_BODY]);
			continue;
		}
		/* Add interesing counts to the header.
		 * Body checksums are always interestig if we have them.
		 * Pretend we always have a basic body checksum. */
		type = g->type;
		if (DCC_CK_IS_BODY(type)) {
			xhdr_add_ck(hdr, type, hdr_tgts.v[type]);
			continue;
		}
		if (hdr_tgts.v[type] != 0)
			xhdr_add_ck(hdr, type, hdr_tgts.v[type]);
	}

	return result;
}



/* check message's checksums in whiteclnt for dccproc or dccsight */
u_char					/* 1=ok 0=something to complain about */
unthr_ask_white(DCC_EMSG *emsg,
		ASK_ST *ask_stp,
		FLTR_SWS *swsp,
		const char *white_nm,
		GOT_CKS *cks,
		CKS_WTGTS *wtgts)
{
	WHITE_LISTING listing;
	int retval;

	/* assume DNS lists are on unless turned off, because there is no reason
	 * to use `dccproc -B` if you don't want to use them */
	*swsp |= FLTR_SW_DNSBL_M;

	/* fake whiteclnt if not specified */
	if (!white_nm) {
		merge_tholds(&cks->tholds_rej, &tholds_rej, 0);
		return 1;
	}

	/* don't filter if something is wrong with the file */
	if (!dcc_new_white_nm(emsg, &cmn_wf, white_nm)) {
		*ask_stp |= ASK_ST_WLIST_NOTSPAM | ASK_ST_LOGIT;
		return 0;
	}

	/* let whiteclnt file turn off the DCC and other filters */
	*swsp = wf2sws(*swsp, &cmn_wf);

	/* combine the command-line thresholds with the thresholds from
	 * from the common /var/dcc/whiteclnt file */
	merge_tholds(&cks->tholds_rej, &tholds_rej, cmn_wf.wtbl);

	retval = 1;
	switch (wf_cks(emsg, &cmn_wf, cks, wtgts, &listing)) {
	case WHITE_OK:
	case WHITE_NOFILE:
		break;
	case WHITE_SILENT:
		*ask_stp |= ASK_ST_LOGIT;
		break;
	case WHITE_COMPLAIN:
	case WHITE_CONTINUE:
		retval = 0;
		*ask_stp |= ASK_ST_LOGIT;
		break;
	}

	switch (listing) {
	case WHITE_LISTED:
		/* do not send whitelisted checksums to DCC server */
		*ask_stp |= ASK_ST_WLIST_NOTSPAM;
		break;
	case WHITE_USE_DCC:
	case WHITE_UNLISTED:
		if (*swsp & FLTR_SW_TRAP)
			*ask_stp |= (ASK_ST_CLNT_ISSPAM | ASK_ST_WLIST_ISSPAM
				     | ASK_ST_LOGIT);
		break;
	case WHITE_BLACK:
		*ask_stp |= (ASK_ST_WLIST_ISSPAM
			     | ASK_ST_CLNT_ISSPAM | ASK_ST_LOGIT);
		break;
	}

	if (*swsp & FLTR_SW_LOG_ALL)
		*ask_stp |= ASK_ST_LOGIT;

	return retval;
}



/* ask the DCC for dccproc or dccsight but not dccifd or dccm */
u_char					/* 1=ok 0=something to complain about */
unthr_ask_dcc(DCC_EMSG *emsg,
	      DCC_CLNT_CTXT *ctxt,
	      DCC_HEADER_BUF *hdr,	/* put header here */
	      ASK_ST *ask_stp,		/* put state bites here */
	      FLTR_SWS sws,
	      GOT_CKS *cks,		/* these checksums */
	      u_char spam,		/* spam==0 && local_tgts==0 --> query */
	      DCC_TGTS local_tgts)	/* number of addressees */
{
	if (*ask_stp & ASK_ST_WLIST_NOTSPAM) {
		if (spam) {
			/* if dccproc says it is spam, then it is, even if
			 * the whiteclnt file says we cannot report it */
			*ask_stp |= (ASK_ST_CLNT_ISSPAM | ASK_ST_LOGIT);
			xhdr_init(hdr, 0);
			xhdr_add_ck(hdr, DCC_CK_BODY, DCC_TGTS_TOO_MANY);
		} else {
			xhdr_whitelist(hdr);
		}
		/* honor log threshold for whitelisted messages */
		dcc_honor_log_cnts(ask_stp, cks, local_tgts);
		return 1;

	} else {
		/* if allowed by whitelisting, report our checksums to the DCC
		 * and return with that result including setting logging */
		return (0 < ask_dcc(emsg, ctxt, DCC_CLNT_FG_NONE,
				    hdr, cks, ask_stp, spam,
				    sws & FLTR_SW_REP_ON, local_tgts,
				    local_tgts));
	}
}



/* parse -g for dccm and dccproc */
void
dcc_parse_honor(const char *arg0)
{
	const char *arg;
	DCC_CK_TYPES type, t2;
	int i;

	arg = arg0;
	if (!CLITCMP(arg, "not_") || !CLITCMP(arg, "not-")) {
		arg += LITZ("not_");
		i = 0;
	} else if (!CLITCMP(arg, "no_") || !CLITCMP(arg, "no-")) {
		arg += LITZ("no_");
		i = 0;
	} else {
		i = 1;
	}

	/* allow -g for ordinary checksums but not reputations or greylisting */
	type = dcc_str2type_thold(arg, -1);
	if (type == DCC_CK_INVALID) {
		dcc_error_msg("unrecognized checksum type in \"-g %s\"",
			      arg0);
		return;
	}
	for (t2 = DCC_CK_TYPE_FIRST; t2 <= DCC_CK_TYPE_LAST; ++t2) {
		if (t2 == type
		    || (type == SET_ALL_THOLDS && IS_ALL_CKSUM(t2))
		    || (type == SET_CMN_THOLDS && IS_CMN_CKSUM(t2)))
			dcc_honor_nospam[t2] = i;
	}
}



void
dcc_clear_tholds(void)
{
	DCC_CK_TYPES type;

	memset(dcc_honor_nospam, 0, sizeof(dcc_honor_nospam));
	dcc_honor_nospam[DCC_CK_IP] = 1;
	dcc_honor_nospam[DCC_CK_ENV_FROM] = 1;
	dcc_honor_nospam[DCC_CK_FROM] = 1;

	for (type = DCC_CK_TYPE_FIRST; type <= DCC_CK_TYPE_LAST; ++type) {
		tholds_log.t[type] = THOLD_UNSET;
		tholds_rej.t[type] = THOLD_UNSET;
	}

	tholds_rej.t[DCC_CK_REP_TOTAL] = DCC_REP_TOTAL_THOLD_DEF;
}



/* Merge thresholds from a whitelist into an existing set.
 *	All settings (as opposed to missing settings) from the
 *	whitelist are copied into the output, possibly overriding
 *	settings in the input. */
u_char					/* 1=merged from whiteclnt wtbl */
merge_tholds(CKSUM_THOLDS *out,
	     const CKSUM_THOLDS *in,
	     const WHITE_TBL *wtbl)
{
	DCC_CK_TYPES type;
	DCC_TGTS tgts;
	u_char result;

	if (in != out)
		*out = *in;
	if (!wtbl)
		return 0;

	result = 0;
	for (type = DCC_CK_TYPE_FIRST; type <= DCC_CK_TYPE_LAST; ++type) {
		tgts = wtbl->hdr.tholds_rej.t[type];
		if (tgts != THOLD_UNSET) {
			out->t[type] = tgts;
			result = 1;
		}
	}
	return result;
}



/* parse type,[log-thold,]rej-thold */
u_char					/* 1=need a log directory */
dcc_parse_tholds(const char *f,		/* "-c " or "-t " */
		 const char *arg)	/* optarg */
{
	DCC_CK_TYPES type;
	DCC_TGTS log_tgts, rej_tgts;
	char *thold_rej, *thold_log;
	u_char log_tgts_set, rej_tgts_set, have_thold_log;

	thold_log = strchr(arg, ',');
	if (!thold_log) {
		dcc_error_msg("missing comma in \"%s%s\"", f, arg);
		return 0;
	}
	type = dcc_str2type_thold(arg, thold_log-arg);
	if (type == DCC_CK_INVALID) {
		dcc_error_msg("unrecognized checksum type in \"%s%s\"", f, arg);
		return 0;
	}

	thold_log = dcc_strdup(++thold_log);

	/* if there is only one threshold, take it as the spam threshold */
	thold_rej = strchr(thold_log, ',');
	if (!thold_rej) {
		thold_rej = thold_log;
		have_thold_log = 0;
	} else {
		*thold_rej++ = '\0';
		have_thold_log = 1;
	}

	log_tgts_set = log_tgts = 0;
	if (have_thold_log && *thold_log != '\0') {
		log_tgts = dcc_str2thold(type, thold_log);
		if (log_tgts == DCC_TGTS_INVALID)
			dcc_error_msg("unrecognized logging threshold"
				      " \"%s\" in \"%s%s\"",
				      thold_log, f, arg);
		else
			log_tgts_set = 1;
	}


	rej_tgts_set = rej_tgts = 0;
	if (!thold_rej || *thold_rej == '\0') {
		if (!have_thold_log || *thold_log == '\0')
			dcc_error_msg("no thresholds in \"%s%s\"", f, arg);
	} else {
		rej_tgts = dcc_str2thold(type, thold_rej);
		if (rej_tgts == DCC_TGTS_INVALID)
			dcc_error_msg("unrecognized rejection threshold"
				      " \"%s\" in \"%s%s\"",
				      thold_rej, f, arg);
		else
			rej_tgts_set = 1;
	}


	if (log_tgts_set || rej_tgts_set) {
		DCC_CK_TYPES t2;

		for (t2 = DCC_CK_TYPE_FIRST; t2 <= DCC_CK_TYPE_LAST; ++t2) {
			if (t2 == type
			    || (type == SET_ALL_THOLDS && IS_ALL_CKSUM(t2))
			    || (type == SET_CMN_THOLDS && IS_CMN_CKSUM(t2))) {
				if (log_tgts_set)
					tholds_log.t[t2] = log_tgts;
				if (rej_tgts_set)
					tholds_rej.t[t2] = rej_tgts;
			}
		}
	}

	dcc_free(thold_log);
	return log_tgts_set;
}



static void
honor_cnt(const GOT_CKS *cks,
	  ASK_ST *ask_stp,		/* previous flag bits */
	  DCC_CK_TYPES type,		/* which kind of checksum */
	  DCC_TGTS type_tgts)		/* total count for the checksum */
{
	if (type >= DIM(dcc_honor_nospam))
		return;

	/* reject and log spam */
	if (cks->tholds_rej.t[type] <= DCC_TGTS_TOO_MANY
	    && cks->tholds_rej.t[type] <= type_tgts
	    && type_tgts <= DCC_TGTS_TOO_MANY) {
		*ask_stp |= (ASK_ST_SRVR_ISSPAM | ASK_ST_LOGIT);
		return;
	}

	/* log messages that are bulkier than the log threshold */
	if (tholds_log.t[type] <= DCC_TGTS_TOO_MANY
	    && tholds_log.t[type] <= type_tgts)
		*ask_stp |= ASK_ST_LOGIT;
}



/* honor log threshold for local counts and white-/blacklists */
void
dcc_honor_log_cnts(ASK_ST *ask_stp,	/* previous flag bits */
		   const GOT_CKS *cks,  /* these server counts */
		   DCC_TGTS tgts)
{
	const GOT_SUM *g;
	DCC_CK_TYPES type;

	if (*ask_stp & ASK_ST_LOGIT)
		return;

	if (tgts == DCC_TGTS_TOO_MANY) {
		*ask_stp |= ASK_ST_LOGIT;
		return;
	}

	/* pretend we always have a body checksum for the log threshold */
	if (tholds_log.t[DCC_CK_BODY] <= DCC_TGTS_TOO_MANY
	    && tholds_log.t[DCC_CK_BODY] <= tgts) {
		*ask_stp |= ASK_ST_LOGIT;
		return;
	}

	for (g = cks->sums; g <= LAST(cks->sums); ++g) {
		type = g->type;
		if (type == DCC_CK_INVALID
		    || type == DCC_CK_ENV_TO)
			continue;
		if (tholds_log.t[type] > DCC_TGTS_TOO_MANY)
			continue;
		if (tholds_log.t[type] <= tgts) {
			*ask_stp |= ASK_ST_LOGIT;
			return;
		}
	}
}



/* compute switch settings from bits in a whiteclnt file */
FLTR_SWS
wf2sws(FLTR_SWS sws, const WF *wf)
{
	static time_t complained;
	time_t now;
	DCC_PATH abs_nm;
	int i;

	if (!grey_on
	    && (wf->wtbl_sws & (WHITE_SW_GREY_ON | WHITE_SW_GREY_LOG_ON))
	    && (now = time(0)) > complained+24*60*60) {
		complained = now;
		dcc_error_msg("%s wants greylisting"
			      " but it is turned off",
			      dcc_fnm2abs_msg(&abs_nm, wf->ascii_nm.c));
	}

	/* compute switch values from whiteclnt bits */

	if (wf->wtbl_sws & WHITE_SW_DISCARD_NO)
		sws |= FLTR_SW_DISCARD_NO;
	else if (wf->wtbl_sws & WHITE_SW_DISCARD_OK)
		sws &= ~FLTR_SW_DISCARD_NO;

	if ((wf->wtbl_sws & WHITE_SW_DCC_OFF))
		sws |= FLTR_SW_DCC_OFF;
	else if (wf->wtbl_sws & WHITE_SW_DCC_ON)
		sws &= ~FLTR_SW_DCC_OFF;

	if (grey_on && (wf->wtbl_sws & WHITE_SW_GREY_ON)) {
		sws &= ~FLTR_SW_GREY_OFF;
	} else if (!grey_on || (wf->wtbl_sws & WHITE_SW_GREY_OFF)) {
		sws |= FLTR_SW_GREY_OFF;
	}

	if (wf->wtbl_sws & WHITE_SW_GREY_SPAM_ON) {
		sws |= FLTR_SW_GREY_IGN_SPAM;
	} else if (wf->wtbl_sws & WHITE_SW_GREY_SPAM_OFF) {
		sws &= ~FLTR_SW_GREY_IGN_SPAM;
	}

	if (wf->wtbl_sws & WHITE_SW_LOG_ALL) {
		sws |= FLTR_SW_LOG_ALL;
	} else if (wf->wtbl_sws & WHITE_SW_LOG_NORMAL) {
		sws &= ~FLTR_SW_LOG_ALL;
	}

	if (wf->wtbl_sws & WHITE_SW_GREY_LOG_ON) {
		sws &= ~FLTR_SW_GREY_LOG_OFF;
	} else if (wf->wtbl_sws & WHITE_SW_GREY_LOG_OFF) {
		sws |= FLTR_SW_GREY_LOG_OFF;
	}

	if (wf->wtbl_sws & WHITE_SW_LOG_M) {
		sws |= FLTR_SW_LOG_M;
	} else if (wf->wtbl_sws & WHITE_SW_LOG_H) {
		sws |= FLTR_SW_LOG_H;
	} else if (wf->wtbl_sws & WHITE_SW_LOG_D) {
		sws |= FLTR_SW_LOG_D;
	}

	if (wf->wtbl_sws & WHITE_SW_MTA_FIRST) {
		sws |= FLTR_SW_MTA_FIRST;
	} else if (wf->wtbl_sws & WHITE_SW_MTA_LAST) {
		sws &= ~FLTR_SW_MTA_FIRST;
	}

	for (i = 0; i < NUM_DNSBL_GROUPS; ++i) {
		if ((wf->wtbl_sws & WHITE_SW_DNSBL_ON(i))
		    && dnsbls) {
			sws |= FLTR_SW_DNSBL(i);
		} else if (wf->wtbl_sws & WHITE_SW_DNSBL_OFF(i)) {
			sws &= ~FLTR_SW_DNSBL(i);
		}
	}

	if (wf->wtbl_sws & WHITE_SW_REP_ON) {
		sws |= FLTR_SW_REP_ON;
	} else if (wf->wtbl_sws & WHITE_SW_REP_OFF) {
		sws &= ~FLTR_SW_REP_ON;
	}

	if (wf->wtbl_sws & WHITE_SW_TRAP_DIS) {
		sws |= FLTR_SW_TRAP | FLTR_SW_TRAP_DIS;
		sws &= ~FLTR_SW_TRAP_REJ;
	} else if (wf->wtbl_sws & WHITE_SW_TRAP_REJ) {
		sws &= ~FLTR_SW_TRAP_DIS;
		sws |= FLTR_SW_TRAP | FLTR_SW_TRAP_REJ;
	} else if (wf->wtbl_sws & WHITE_SW_TRAP_NOT) {
		sws &= ~(FLTR_SW_TRAP | FLTR_SW_TRAP_DIS | FLTR_SW_TRAP_REJ);
	}

	/* adjust other switches for trapping */
	if (sws & FLTR_SW_TRAP) {
		sws |= (FLTR_SW_GREY_OFF | FLTR_SW_REP_ON);
		/* always report trapped spam
		 * DNS lists are redundant for traps */
		sws &= ~(FLTR_SW_DCC_OFF | FLTR_SW_DNSBL_M);
	}

	return sws | FLTR_SW_SET;
}



#define LOG_ASK_ST_BLEN	    160
#define LOG_ASK_ST_OFF	    "(off)"
#define LOG_ASK_ST_OVF	    " ...\n\n"
static int
log_ask_st_sub(char *buf, int blen,
	       const char *s, int slen,
	       u_char off)
{
	int dlen, tlen;

	/* quit if no room at all in the log */
	if (blen >= LOG_ASK_ST_BLEN)
		return LOG_ASK_ST_BLEN;

	/* quit if nothing to say */
	if (!s || !slen)
		return blen;

	dlen = LOG_ASK_ST_BLEN - blen;
	tlen = LITZ(LOG_ASK_ST_OVF)+2+slen;
	if (off)			/* notice if we need to say "(off)" */
		tlen += LITZ(LOG_ASK_ST_OFF);
	if (dlen <= tlen) {
		/* show truncation of the message with "..." */
		memcpy(&buf[blen], LOG_ASK_ST_OVF, LITZ(LOG_ASK_ST_OVF));
		blen += LITZ(LOG_ASK_ST_OVF);
		if (blen < LOG_ASK_ST_BLEN)
			memset(&buf[blen], ' ', LOG_ASK_ST_BLEN-blen);
		return LOG_ASK_ST_BLEN;
	}

	if (blen > 0 && buf[blen-1] != '\n') {
		buf[blen++] = ' ';
		buf[blen++] = ' ';
	}
	memcpy(buf+blen, s, slen);
	blen += slen;
	if (off) {
		memcpy(buf+blen, LOG_ASK_ST_OFF, LITZ(LOG_ASK_ST_OFF));
		blen += LITZ(LOG_ASK_ST_OFF);
	}
	return blen;
}



/* generate log file line of results */
void
log_ask_st(LOG_WRITE_FNC fnc, void *cp,
	   ASK_ST ask_st,
	   FLTR_SWS and_sws,
	   FLTR_SWS or_sws,
	   u_char log_type,		/* 0="" 1="per-user" 2="global" */
	   const DCC_HEADER_BUF *hdr)
{
	char buf[LOG_ASK_ST_BLEN+3];
	int blen, len, i;
#define S(str,off) (blen = log_ask_st_sub(buf, blen, str, LITZ(str), (off) != 0))
#define S0(bit,off,str) (((ask_st & bit) != 0) ? S(str,off) : 0)
#define S1(bit,off,str) S0(bit,off,str)
#define S2(bit,str) S0(bit,0,str)

	blen = 0;
	S2(ASK_ST_QUERY, "query");

	/* the CGI scripts want to know why */
	if (or_sws & FLTR_SW_MTA_FIRST) {
		S2(ASK_ST_MTA_ISSPAM,	"MTA"DCC_XHDR_ISSPAM);
		S2(ASK_ST_MTA_NOTSPAM,	"MTA"DCC_XHDR_ISOK);
	}

	S2(ASK_ST_WLIST_NOTSPAM,	"wlist"DCC_XHDR_ISOK);
	S2(ASK_ST_WLIST_ISSPAM,		"wlist"DCC_XHDR_ISSPAM);

	S1(ASK_ST_SRVR_ISSPAM, and_sws & FLTR_SW_DCC_OFF, "DCC"DCC_XHDR_ISSPAM);
	S1(ASK_ST_SRVR_NOTSPAM,	and_sws & FLTR_SW_DCC_OFF, "DCC"DCC_XHDR_ISOK);
	S1(ASK_ST_REP_ISSPAM, !(or_sws & FLTR_SW_REP_ON), "Rep"DCC_XHDR_ISSPAM);

	for (i = 0; i < NUM_DNSBL_GROUPS; ++i) {
		char mbuf[24];
		const char *res;

		if (ask_st & (ASK_ST_DNSBL_W(i) | ASK_ST_DNSBL_B(i))) {
			/* log "DNSBLx-->spam" "DNSBLx-->spam(off)"
			*  "DNSBLx-->ok" or "DNSBLx-->ok(off)" */
			res = ((ask_st & ASK_ST_DNSBL_W(i))
			       ? DCC_XHDR_ISOK : DCC_XHDR_ISSPAM);
			if (have_dnsbl_groups)
				len = snprintf(mbuf, sizeof(mbuf),
					       "DNSBL%d%s", i+1, res);
			else
				len = snprintf(mbuf, sizeof(mbuf),
					       "DNSBL%s", res);
			if (len >= ISZ(mbuf))
				len = ISZ(mbuf)-1;
			blen = log_ask_st_sub(buf, blen, mbuf, len,
					      0 == (or_sws & FLTR_SW_DNSBL(i)));
		} else if (ask_st & ASK_ST_DNSBL_TIMEO(i)) {
			if (have_dnsbl_groups)
				len = snprintf(mbuf, sizeof(mbuf),
					       "DNSBL%d(timeout)", i+1);
			else
				len = snprintf(mbuf, sizeof(mbuf),
					       "DNSBL(timeout)");
			if (len >= ISZ(mbuf))
				len = ISZ(mbuf)-1;
			blen = log_ask_st_sub(buf, blen, mbuf, len, 0);
		}
	}

	if (!(or_sws & FLTR_SW_MTA_FIRST)) {
		S2(ASK_ST_MTA_ISSPAM,	"MTA"DCC_XHDR_ISSPAM);
		S2(ASK_ST_MTA_NOTSPAM,  "MTA"DCC_XHDR_ISOK);
	}
	blen = log_ask_st_sub(buf, blen, dcc_progname.c, dcc_progname_len, 0);
	if (log_type == 1) {
		blen = log_ask_st_sub(buf, blen,
				      "per-user-log", LITZ("per-user-log"), 0);
	} else if (log_type == 2) {
		blen = log_ask_st_sub(buf, blen,
				      "global-log", LITZ("global-log"), 0);
	}
	blen = log_ask_st_sub(buf, blen, "\n\n", 2, 0);
	fnc(cp, buf, blen);

	if (hdr->used != 0)
		xhdr_write(fnc, cp, hdr->buf, hdr->used, 0);
#undef S
#undef S0
#undef S1
#undef S2
}



/* parse -G options for DCC clients */
u_char					/* 0=bad */
dcc_parse_client_grey(const char *arg)
{
	int bits;
	const char *p;

	while (*arg != '\0') {
		if (ck_word_comma(&arg, "on")) {
			grey_on = 1;
			continue;
		}
		if (ck_word_comma(&arg, "off")) {
			grey_on = 0;
			continue;
		}
		if (ck_word_comma(&arg, "noIP")) {
			grey_on = 1;
			trim_grey_ip_addr = 1;
			memset(&grey_ip_mask, 0, sizeof(grey_ip_mask));
			continue;
		}
		if (!CLITCMP(arg, "IPmask/")) {
			bits = 0;
			for (p = arg+LITZ("IPmask/");
			     *p >= '0' && *p <= '9';
			     ++p)
				bits = bits*10 + *p - '0';
			if (bits > 0 && bits < 128
			    && (*p == '\0' || *p == ',')) {
				arg = p;
				if (*p == ',')
					++arg;
				grey_on = 1;
				trim_grey_ip_addr = 1;
				/* assume giant blocks are really IPv4 */
				if (bits <= 32)
					bits += 128-32;
				bits2mask(&grey_ip_mask, bits);
				continue;
			}
		}
		return 0;
	}
	return 1;
}



/* sanity check the DCC server's answer */
u_char
dcc_ck_grey_answer(DCC_EMSG *emsg, const DCC_OP_RESP *resp)
{
	int recv_len;

	recv_len = ntohs(resp->hdr.len);
	if (resp->hdr.op != DCC_OP_ANSWER) {
		dcc_pemsg(EX_UNAVAILABLE, emsg, "DCC %s: %s %*s",
			  dcc_srvr_nm(1),
			  dcc_hdr_op2str(0, 0, &resp->hdr),
			  (resp->hdr.op == DCC_OP_ERROR
			   ? (recv_len - (ISZ(resp->error)
					  - ISZ(resp->error.msg)))
			   : 0),
			  resp->error.msg);
		return 0;
	}

	if (recv_len != sizeof(DCC_GREY_ANSWER)) {
		dcc_pemsg(EX_UNAVAILABLE, emsg,
			  "greylist server %s answered with %d instead of"
			  " %d bytes",
			  dcc_srvr_nm(1), recv_len, ISZ(DCC_GREY_ANSWER));
		return 0;
	}

	return 1;
}



ASK_GREY_RESULT
ask_grey(DCC_EMSG *emsg,
	 DCC_CLNT_CTXT *ctxt,
	 DCC_OPS op,			/* DCC_OP_GREY_{REPORT,QUERY,WHITE} */
	 DCC_SUM *msg_sum,		/* put msg+sender+target cksum here */
	 DCC_SUM *triple_sum,		/* put greylist triple checksum here */
	 const GOT_CKS *cks,
	 const DCC_SUM *env_to_sum,
	 DCC_TGTS *pembargo_num,
	 DCC_TGTS *pearly_tgts,		/* ++ report to DCC even if embargoed */
	 DCC_TGTS *plate_tgts)		/* ++ don't report to DCC */
{
	MD5_CTX ctx;
	DCC_REPORT rpt;
	DCC_OP_RESP resp;
	DCC_CK *ck;
	DCC_CK_TYPES type;
	const GOT_SUM *g;
	DCC_TGTS result_tgts;
	int num_cks;

	if (cks->sums[DCC_CK_IP].type != DCC_CK_IP) {
		dcc_pemsg(EX_UNAVAILABLE, emsg,
			  "IP address not available for greylisting");
		memset(triple_sum, 0, sizeof(*triple_sum));
		memset(msg_sum, 0, sizeof(*msg_sum));
		return ASK_GREY_FAIL;
	}
	if (cks->sums[DCC_CK_ENV_FROM].type != DCC_CK_ENV_FROM) {
		dcc_pemsg(EX_UNAVAILABLE, emsg,
			  "env_From not available for greylisting");
		memset(triple_sum, 0, sizeof(*triple_sum));
		memset(msg_sum, 0, sizeof(*msg_sum));
		return ASK_GREY_FAIL;
	}

	/* Check the common checksums for whitelisting at the greylist server.
	 * This assumes DCC_CK_GREY_TRIPLE > DCC_CK_GREY_MSG > other types */
	ck = rpt.cks;
	num_cks = 0;
	for (type = 0, g = cks->sums;
	     type <= DCC_CK_TYPE_LAST;
	     ++type, ++g) {
		/* greylisting needs a body checksum, even if
		 * it is the fake checksum for a missing body */
		if (!g->rpt2srvr && type != DCC_CK_BODY)
			continue;

		/* do not ask the greylist server about reputations */
		if (DCC_CK_IS_REP(0, type))
			continue;

		ck->type = type;
		ck->len = sizeof(*ck);
		ck->sum = g->sum;
		++ck;
		++num_cks;
	}

	/* include in the request the grey message checksum as the checksum
	 * of the body, the env_From sender, and env_To target checksums */
	MD5Init(&ctx);
	MD5Update(&ctx, cks->sums[DCC_CK_BODY].sum.b, sizeof(DCC_SUM));
	MD5Update(&ctx, cks->sums[DCC_CK_ENV_FROM].sum.b, sizeof(DCC_SUM));
	MD5Update(&ctx, env_to_sum->b, sizeof(DCC_SUM));
	MD5Final(msg_sum->b, &ctx);
	ck->type = DCC_CK_GREY_MSG;
	ck->len = sizeof(*ck);
	ck->sum = *msg_sum;
	++ck;
	++num_cks;

	/* include the triple checksum of the sender, the sender's IP
	 * address, and the target */
	MD5Init(&ctx);
	if (trim_grey_ip_addr) {
		struct in6_addr addr;
		DCC_SUM sum;
		apply_ipmask(&addr, &cks->ip_addr, &grey_ip_mask);
		ipv6tock(&sum, &addr);
		MD5Update(&ctx, sum.b, sizeof(DCC_SUM));
	} else {
		MD5Update(&ctx, cks->sums[DCC_CK_IP].sum.b, sizeof(DCC_SUM));
	}
	MD5Update(&ctx, cks->sums[DCC_CK_ENV_FROM].sum.b, sizeof(DCC_SUM));
	MD5Update(&ctx, env_to_sum->b, sizeof(DCC_SUM));
	MD5Final(triple_sum->b, &ctx);
	ck->type = DCC_CK_GREY3;
	ck->len = sizeof(*ck);
	ck->sum = *triple_sum;
	++num_cks;

	if (!dcc_clnt_op(emsg, ctxt, DCC_CLNT_FG_GREY, 0, 0, 0,
			 &rpt.hdr, (sizeof(rpt) - sizeof(rpt.cks)
				    + num_cks*sizeof(rpt.cks[0])),
			 op, &resp, sizeof(resp))) {
		return ASK_GREY_FAIL;
	}

	if (!dcc_ck_grey_answer(emsg, &resp))
		return ASK_GREY_FAIL;

	/* see what the greylist server had to say */
	result_tgts = ntohl(resp.gans.triple);
	switch (result_tgts) {
	case DCC_TGTS_OK:		/* embargo ended just now */
		/* if we have previously included this target in a count of
		 * targets sent to the DCC, then do not include it now */
		if (resp.gans.msg != 0 && plate_tgts)
			++*plate_tgts;
		if (pembargo_num)
			*pembargo_num = 0;
		return ASK_GREY_EMBARGO_END;

	case DCC_TGTS_TOO_MANY:		/* no current embargo */
		if (pembargo_num)
			*pembargo_num = 0;
		return ((resp.gans.msg != 0)
			? ASK_GREY_EMBARGO_END
			: ASK_GREY_PASS);

	case DCC_TGTS_GREY_WHITE:	/* whitelisted for greylisting */
		if (pembargo_num)
			*pembargo_num = 0;
		return ASK_GREY_WHITE;

	default:			/* embargoed */
		/* if this is a brand new embargo,
		 * then count this target in the DCC report */
		if (resp.gans.msg == 0 && pearly_tgts)
			++*pearly_tgts;
		if (pembargo_num)
			*pembargo_num = result_tgts+1;
		return ASK_GREY_EMBARGO;
	}
}
