/* Distributed Checksum Clearinghouse
 *
 * DCC interface daemon
 *
 * Copyright (c) 2005 by Rhyolite Software
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE DISCLAIMS ALL
 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE
 * 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.2.74-1.61 $Revision$
 */

#include "dccif.h"
#include "cmn_defs.h"
#include "dcc_paths.h"
#include <signal.h>


static int stopping;

#define MAX_SMTP_LINE

#if defined(NO_IPV6) || !defined(USE_IPV6)
u_char use_ipv6 = 0;
#else
u_char use_ipv6 = 2;
#endif

/* incoming proxy and bidirectional ASCII dccifd protocol connection */
static const char *listen_addr;
static u_char listen_family;
static struct sockaddr_un listen_sun;
static const char *raddr_rmask;
static struct in6_addr raddr, rmask;
static SOCKET listen_soc = -1;

/* outgoing proxy connection */
static u_char proxy_out_family;
static struct sockaddr_un proxy_out_sun;
static char proxy_out_host[MAXHOSTNAMELEN];
static u_int16_t proxy_out_port;
static DCC_SOCKU proxy_out_su;

static const char *homedir = 0;
static const char *rundir = DCC_RUNDIR;
static DCC_PATH pidpath;
static const char *progpath = DCC_LIBEXECDIR"/dccifd";

static const char *tmpdir;

static u_char background = 1;

static u_char proxy;


/* message state or context */
typedef struct {			/* control an input buffer */
    char	*base;
    char	*in;
    char	*out;
    char	*next_line;
    int		size;
    int		line_len;
    int		*socp;
} IN_BC;
typedef struct {			/* control an ouput buffer */
    char	*base;
    char	*in;
    int		len;
    int		size;
    int		*socp;
} OUT_BC;
typedef struct work {
    int		proxy_in_soc;
    DCC_SOCKU   proxy_in_su;
    int		proxy_out_soc;
    char	env_from[DCC_HDR_CK_MAX+1];
    int		tmp_fd;			/* to send a copy back to the MTA */
    char	buf1[DCC_HDR_CK_MAX*8]; /* >=DCC_HDR_CK_MAX*2 & MAX_RCPTS */
    char	buf2[DCC_HDR_CK_MAX*8];
    char	buf3[1024];
    char	buf4[1024];
    CMN_WORK    cw;
    enum {
	SMTP_ST_RSET,			/* expecting Mail_From */
	SMTP_ST_TRANS,			/* seen Mail_From */
	SMTP_ST_RCPT,			/* seen Rcpt_To */
	SMTP_ST_ERROR,			/* no transaction until RSET or HELO */
    } smtp_state;
    DCC_PATH    tmp_nm;
    /* from here down is zeroed when the structure is allocated */
#define WORK_ZERO fwd
    struct work *fwd;
    IN_BC       msg_rd;			/* incoming mail message */
    OUT_BC      msg_wt;			/* outoing mail message  */
    IN_BC       reply_in;		/* incoming SMTP replies */
    OUT_BC      reply_out;		/* outgoing SMTP replies */
    int		total_hdrs, cr_hdrs;
    u_int       flags;
#    define      FG_WORK_LOCK       0x0001  /* hold the lock */
#    define      FG_MISSING_BODY    0x0002  /* missing message body */
#    define      FG_MTA_BODY	    0x0004  /* MTA wants the body */
#    define      FG_MTA_HEADER      0x0008  /* MTA wants the X-DCC header */
#    define      FG_MTA_DCC_QUERY   0x0010  /* MTA only wants to query DCC */
#    define      FG_MTA_GREY_QUERY  0x0020  /* MTA wants query DCC greylist */
#    define      FG_SEEN_HDR	    0x0040  /* have at least 1 header */
#    define      FG_CK_RCVD	    0x0080  /* check 1st Received header */
#    define	 FG_XCLIENT_NAME    0x0100   /* client name via XCLIENT */
#    define	 FG_XCLIENT_ADDR    0x0200   /* client address via XCLIENT */
#    define	 FG_XCLIENT_HELO    0x0400   /* HELO value via XCLIENT */
#    define	 FG_RECYCLE (FG_WORK_LOCK | FG_MTA_BODY			\
			     | FG_XCLIENT_NAME | FG_XCLIENT_ADDR	\
			     | FG_XCLIENT_HELO)
} WORK;

/* use a free list to avoid malloc() overhead */
static WORK *work_free;

/* Every job involves
 *      a socket connected to the MTA or upstream proxy
 *      a socket connected to the downstream proxy if using SMTP
 *      a log file,
 *      and a socket to talk to the DCC server.
 * While processing per-user whitelists there are
 *      another log file
 *      and an extra file descriptor for the main log file.
 * The file descriptors for the whitelists are accounted for in EXTRA_FILES */
#define FILES_PER_JOB   6


#define MAX_SELECT_WORK ((FD_SETSIZE-EXTRA_FILES)/FILES_PER_JOB)
#define DEF_MAX_WORK    200
#define MIN_MAX_WORK    2
static int max_max_work = MAX_SELECT_WORK;
static int max_work = DEF_MAX_WORK;

typedef struct user_domain {
    struct user_domain *fwd;
    const char	*nm;
    int		len;
} USER_DOMAIN;
static USER_DOMAIN *user_domains;

/* max seconds to wait for MTA
 *      Longer than the longest timeout in RFC 2821 */
#define MAX_MTA_DELAY   (10*60+5)


static void sigterm(int);
static u_char set_soc(DCC_EMSG, int, int, const char *);
static void bind_unix_listen(void);
static void bind_tcp_listen(void);
static void close_listen_soc(void);
static void unlink_listen_sun(void);
static void *job_start(void *);
static void job_close(WORK *);
static void NRATTRIB job_exit(WORK *);


static void
usage(u_char die)
{
	const char str[] = {
	    "usage: [-VdbxANQW] [-G on | off | noIP | IPmask/xx] [-h homedir]\n"
	    "    [-p /sock | host,port,rhost/bits]"
	    " [-o /sock | host,port]\n"
	    "    [-D local-domain] [-r rejection-msg] [-m map] [-w whiteclnt]\n"
	    "    [-U userdirs] [-a IGNORE|REJECT]"
	    " [-t type,[log-thold,][spam-thold]]\n"
	    "    [-g [not-]type] [-S header] [-l logdir] [-R rundir]"
	    " [-T tmpdir]\n"
	    "    [-j maxjobs] [-B dnsbl-option] [-L ltype,facility.level]"
	};
	static u_char complained;

	/* its important to try to run, so don't give up unless necessary */
	if (die) {
		dcc_logbad(EX_USAGE, complained ? "giving up" : str);
	} else if (!complained) {
		dcc_error_msg("%s\ncontinuing", str);
		complained = 1;
	}
}


int NRATTRIB
main(int argc, char **argv)
{
	DCC_EMSG emsg;
#ifdef HAVE_RLIMIT_NOFILE
	struct rlimit nofile;
	int old_rlim_cur;
#endif
	FILE *f;
	long l;
	u_char log_tgts_set = 0;
	const char *logdir = 0;
	WORK *wp;
	pthread_t tid;
	int namelen;
	const char *cp;
	USER_DOMAIN *udom, *udom2, **udomp;
	char *p;
	int error, i;

	emsg[0] = '\0';
	if (*argv[0] == '/')
		progpath = argv[0];
	dcc_syslog_init(1, argv[0], 0);
	dcc_init_tholds();

#ifdef HAVE_RLIMIT_NOFILE
	if (0 > getrlimit(RLIMIT_NOFILE, &nofile)) {
		dcc_error_msg("getrlimit(RLIMIT_NOFILE): %s", ERROR_STR());
		old_rlim_cur = 1000*1000;
	} else {
		old_rlim_cur = nofile.rlim_cur;
		if (nofile.rlim_max < 1000*1000) {
			i = nofile.rlim_max;
#ifndef USE_POLL
			if (i > FD_SETSIZE)
				i = FD_SETSIZE;
#endif
			max_max_work = (i - EXTRA_FILES)/FILES_PER_JOB;
			if (max_max_work <= 0) {
				dcc_error_msg("only %d open files allowed",
					      (int)nofile.rlim_max);
				max_max_work = 1;
			}
		}
	}
#else /* HAVE_RLIMIT_NOFILE */
	if (max_work <= 0) {
		dcc_error_msg(EX_OSERR, "too few open files allowed");
		max_max_work = 1;
	}
#endif /* HAVE_RLIMIT_NOFILE */
	max_work = min(DEF_MAX_WORK, max_max_work);

	while (EOF != (i = getopt(argc, argv,
				  "64VdbxANQW"
				  "G:h:p:o:D:r:m:w:U:a:t:g:S:l:R:T:j:B:L:"))) {
		switch (i) {
#ifndef NO_IPV6
		case '6':
			use_ipv6 = 1;
			break;
#endif
		case '4':
			use_ipv6 = 0;
			break;

		case 'V':
			fprintf(stderr, DCC_VERSION"\n");
			exit(EX_OK);
			break;

		case 'd':
			++dcc_clnt_debug;
			break;

		case 'b':
			background = 0;
			break;

		case 'x':
			try_extra_hard = DCC_CLNT_FG_NO_FAIL;
			break;

		case 'A':
			chghdr = ADDHDR;
			break;

		case 'N':
			chghdr = NOHDR;
			break;

		case 'Q':
			dcc_query_only = 1;
			break;

		case 'G':
			if (!dcc_parse_client_grey(optarg))
				usage(0);
			break;

		case 'W':
			to_white_only = 1;
			break;

		case 'h':
			homedir = optarg;
			break;

		case 'p':
			listen_addr = optarg;
			break;

		case 'o':
			proxy = 1;
			p = strchr(optarg, ',');
			if (!p) {
				/* recognize single-ended (not-really-)-proxy */
				if (!strcmp(optarg, "/dev/null")) {
					proxy_out_family = AF_UNSPEC;
					break;
				}
				if (strlen(optarg)>=ISZ(proxy_out_sun.sun_path))
					dcc_logbad(EX_USAGE, "invalid UNIX"
						   " domain socket: -o %s",
						   optarg);
				strcpy(proxy_out_sun.sun_path, optarg);
#ifdef DCC_HAVE_SA_LEN
				proxy_out_sun.sun_len = SUN_LEN(&proxy_out_sun);
#endif
				proxy_out_sun.sun_family = AF_UNIX;
				proxy_out_family = AF_UNIX;
			} else {
				cp = dcc_parse_nm_port(emsg, optarg,
						       DCC_GET_PORT_INVALID,
						       proxy_out_host,
						       sizeof(proxy_out_host),
						       &proxy_out_port, 0, 0,
						       0, 0);
				if (!cp)
					dcc_logbad(dcc_ex_code, "%s", emsg);
				if (*cp != '\0')
					dcc_logbad(EX_USAGE,
						   "invalid IP address: \"%s\"",
						   optarg);
				if (proxy_out_host[0] == '\0'
				    || !strcmp(proxy_out_host, "@")) {
					/* null or "@" means incoming host */
					;
				} else {
					dcc_host_lock();
					if (!dcc_get_host(proxy_out_host,
							use_ipv6, &error))
					    dcc_logbad(EX_NOHOST, "%s: %s",
						       proxy_out_host,
						       DCC_HSTRERROR(error));
					proxy_out_su = dcc_hostaddrs[0];
					*DCC_SU_PORT(&proxy_out_su
						     ) = proxy_out_port;
					dcc_host_unlock();
				}
				proxy_out_family = AF_INET;
			}
			break;

		case 'D':
			/* save user domain names sorted  by length */
			if (*optarg == '\0'
			    || !strcmp(optarg, "@")
			    || strchr(optarg+1, '@')) {
				dcc_logbad(EX_USAGE,
					   "invalid local-domain \"%s\"",
					   optarg);
				break;
			}
			udom2 = dcc_malloc(sizeof(*udom2));
			udom2->nm = optarg;
			i = strlen(optarg);
			udom2->len = i;
			udomp = &user_domains;
			for (;;) {
				udom = *udomp;
				if (!udom || i > udom->len) {
					udom2->fwd = udom;
					*udomp = udom2;
					break;
				}
				udomp = &udom->fwd;
			}
			break;

		case 'r':
			parse_reply_arg(optarg);
			break;

		case 'm':
			mapfile_nm = optarg;
			break;

		case 'w':
			main_white_nm = optarg;
			break;

		case 'U':
			parse_userdirs(optarg);
			break;

		case 'a':
			if (!strcasecmp(optarg, "IGNORE")) {
				action = CMN_IGNORE;
			} else if (!strcasecmp(optarg, "REJECT")) {
				action = CMN_REJECT;
			} else if (!strcasecmp(optarg, "DISCARD")) {
				action = CMN_DISCARD;
			} else {
				dcc_error_msg("unrecognized -a action: %s",
					      optarg);
			}
			break;

		case 't':
			if (dcc_parse_tholds('t', optarg))
				log_tgts_set = 1;
			break;

		case 'g':		/* honor not-spam "counts" */
			dcc_parse_honor(optarg);
			break;

		case 'S':
			dcc_add_sub_hdr(0, optarg);
			break;

		case 'l':		/* log rejected mail here */
			logdir = optarg;
			break;

		case 'R':
			rundir = optarg;
			break;

		case 'T':
			tmpdir = optarg;
			break;

		case 'j':		/* maximum simultaneous jobs */
			l = strtoul(optarg, &p, 0);
			if (*p != '\0' || l < MIN_MAX_WORK) {
				dcc_error_msg("invalid queue length %s",
					      optarg);
			} else if (l > max_max_work) {
				dcc_error_msg("queue length %s"
					      " larger than limit %d",
					      optarg, max_max_work);
				max_work = max_max_work;
			} else {
				max_work = l;
			}
			break;

		case 'B':
			if (!dcc_parse_dnsbl(emsg, optarg, progpath))
				dcc_error_msg("%s", emsg);
			break;

		case 'L':
			dcc_parse_log_opt(optarg);
			break;

		default:
			usage(0);
		}
	}
	argc -= optind;
	argv += optind;
	if (argc != 0)
		usage(0);

	if (!dcc_cdhome(emsg, homedir))
		dcc_error_msg("%s", emsg);

	/* Open the incoming socket before our backgrounding fork() to
	 * minimize races and allow better error reporting.
	 *
	 * It is a TCP address if it has a port number and mask.
	 * Otherwise it is a UNIX domain socket.
	 */
	if (listen_addr != 0
	    && (p = strchr(listen_addr, ',')) != 0
	    && strchr(p, ',')) {
		bind_tcp_listen();
	} else {
		bind_unix_listen();
	}
	if (!set_soc(emsg, listen_soc, listen_family, "main socket"))
		dcc_logbad(dcc_ex_code, "%s", emsg);
	if (0 > listen(listen_soc, 10))
		dcc_logbad(EX_IOERR, "listen(): %s", ERROR_STR());

	if (logdir)
		dcc_log_init(0, logdir);
	if (!dcc_have_logdir
	    && log_tgts_set)
		dcc_error_msg("log thresholds set with -t but no -l directory");

	if (!tmpdir)
		tmpdir = dcc_have_logdir ? dcc_logdir : _PATH_TMP;

#ifdef HAVE_RLIMIT_NOFILE
	if (old_rlim_cur < (i = max_work*FILES_PER_JOB+EXTRA_FILES)) {
		nofile.rlim_cur = i;
		if (0 > setrlimit(RLIMIT_NOFILE, &nofile)) {
			dcc_error_msg("setrlimit(RLIMIT_NOFILE,%d): %s",
				      i, ERROR_STR());
			max_work = old_rlim_cur/FILES_PER_JOB - EXTRA_FILES;
			if (max_work <= 0) {
				dcc_error_msg("only %d open files allowed",
					      old_rlim_cur);
				max_work = MIN_MAX_WORK;
			}
		}
	}
#endif /* HAVE_RLIMIT_NOFILE */

	finish_replies();

	/* Create the contexts. */
	i = max_work;
	wp = dcc_malloc(sizeof(*wp)*i);
	work_free = wp;
	memset(wp, 0, sizeof(*wp)*i);
	while (--i > 0) {
		wp->proxy_in_soc = -1;
		wp->proxy_out_soc = -1;
		wp->tmp_fd = -1;
		cmn_create(&wp->cw);
		wp->fwd = wp+1;
		++wp;
	}
	wp->proxy_in_soc = -1;
	wp->proxy_out_soc = -1;
	wp->tmp_fd = -1;
	cmn_create(&wp->cw);

	rcpts_create(max_work*2);

	if (background) {
		if (daemon(1, 0) < 0)
			dcc_logbad(EX_OSERR, "daemon(): %s", ERROR_STR());
		snprintf(pidpath, sizeof(pidpath), "%s/%s.pid",
			 rundir, dcc_progname);
		unlink(pidpath);
		f = fopen(pidpath, "w");
		if (!f) {
			dcc_error_msg("fopen(%s): %s",
				      pidpath, ERROR_STR());
		} else {
			fprintf(f, "%d\n", (u_int)getpid());
			fclose(f);
		}
	}

	signal(SIGPIPE, SIG_IGN);
	signal(SIGHUP, sigterm);
	signal(SIGTERM, sigterm);
	signal(SIGINT, sigterm);

	/* Be careful to start all threads only after the fork() in daemon(),
	 * because some POSIX threads packages (e.g. FreeBSD) get confused
	 * about threads in the parent.  */

	cmn_init(emsg);

	if (listen_family != AF_UNIX) {
		dcc_trace_msg(DCC_VERSION" listening to %s from %s",
			      listen_addr, raddr_rmask);
	} else {
		dcc_trace_msg(DCC_VERSION" listening to %s",
			      listen_addr);
	}
	if (dcc_clnt_debug)
		dcc_trace_msg("max_work=%d max_max_work=%d",
			      max_work, max_max_work);

	while (!stopping) {
		/* delay for 1 second instead of forever to notice
		 * when SIGTERM has said to stop */
		i = dcc_select_poll(emsg, listen_soc, 1, DCC_USECS);
		if (i < 0)
			dcc_logbad(EX_OSERR, "%s", emsg);
		if (i == 0)
			continue;

		/* A new connection is ready.  Allocate a context
		 * block and create a thread */
		lock_work();
		wp = work_free;
		if (!wp) {
			/* pretend we weren't listening if we
			 * are out of context blocks */
			unlock_work();
			sleep(1);
			continue;
		}
		work_free = wp->fwd;
		unlock_work();

		/* clear most of it */
		cmn_clear(&wp->cw, wp);
		memset(&wp->WORK_ZERO, 0,
		       sizeof(*wp) - ((char*)&wp->WORK_ZERO - (char*)wp));

		namelen = sizeof(wp->proxy_in_su);
		wp->proxy_in_soc = accept(listen_soc,
					  &wp->proxy_in_su.sa, &namelen);
		if (wp->proxy_in_soc < 0) {
			dcc_error_msg("accept(): %s", ERROR_STR());
			job_close(wp);
			continue;
		}
		if (listen_family != AF_UNIX) {
			struct in6_addr addr6, *ap;

			if (wp->proxy_in_su.sa.sa_family == AF_INET6) {
				ap = &wp->proxy_in_su.ipv6.sin6_addr;
			} else {
				ap = &addr6;
				dcc_ipv4toipv6(ap,
					       wp->proxy_in_su.ipv4.sin_addr);
			}
			if (!DCC_IN_BLOCK(*ap, raddr, rmask)) {
				dcc_error_msg("unauthorized client address %s",
					      dcc_su2str(&wp->proxy_in_su));
				job_close(wp);
				continue;
			}
		}
		if (!set_soc(emsg, wp->proxy_in_soc, listen_family, "MTA")) {
			dcc_error_msg("%s", emsg);
			job_close(wp);
			continue;
		}
		pthread_create(&tid, 0, job_start, wp);
		i = pthread_detach(tid);
		if (i != 0) {
		   if (i != ESRCH)
			   dcc_error_msg("pthread_detach(): %s", ERROR_STR1(i));
		   else if (dcc_clnt_debug)
			   dcc_trace_msg("pthread_detach(): %s", ERROR_STR1(i));
		}
	}
	totals_msg();

	close_listen_soc();
	if (pidpath[0] != '\0')
		unlink(pidpath);

	exit(-stopping);
}



/* brag about our accomplishments */
void
totals_msg(void)
{
	time_t now;
	struct tm tm;
	char tbuf[20];

	lock_work();
	now = time(0);
	strftime(tbuf, sizeof(tbuf), "%x %X",
		 dcc_localtime(totals.msg_prev, &tm));
	if (grey_on) {
		dcc_trace_msg(DCC_VERSION
			      " greylisted %d messages,"
			      " rejected messages to %d targets and"
			      " discarded messages to %d targets among"
			      " %d total since %s",
			      totals.tgts_embargoed,
			      totals.tgts_rejected,
			      totals.tgts_discarded,
			      totals.tgts, tbuf);
	} else {
		dcc_trace_msg(DCC_VERSION
			      " rejected messages to %d targets and"
			      " discarded messages to %d targets among"
			      " %d total since %s",
			      totals.tgts_rejected,
			      totals.tgts_discarded,
			      totals.tgts, tbuf);
	}
	memset(&totals, 0, sizeof(totals));
	totals.msg_prev = now;
	unlock_work();
}



static void
unlink_listen_sun(void)
{
	if (listen_family != AF_UNIX)
		return;

	/* there is an unavoidable race here */
	if (0 > unlink(listen_sun.sun_path)
	    && errno != ENOENT)
		dcc_error_msg("unlink(%s): %s",
			      listen_sun.sun_path, ERROR_STR());
}



static void
bind_unix_listen(void)
{
	DCC_EMSG emsg;
	struct stat sb;

	emsg[0] = '\0';
	listen_family = AF_UNIX;

	if (!listen_addr) {
		/* use default UNIX domain socket */
		snprintf(listen_sun.sun_path, sizeof(listen_sun.sun_path),
			 "%s/"DCC_DCCIF_UDS, dcc_homedir);
		listen_addr = listen_sun.sun_path;
	} else {
		if (strlen(listen_addr) >= ISZ(listen_sun.sun_path))
			dcc_logbad(EX_USAGE, "invalid UNIX domain socket: %s",
				   listen_addr);
		strcpy(listen_sun.sun_path, listen_addr);
	}
#ifdef DCC_HAVE_SA_LEN
	listen_sun.sun_len = SUN_LEN(&listen_sun);
#endif
	listen_sun.sun_family = AF_UNIX;

	if (0 <= stat(listen_sun.sun_path, &sb)
	    && !(S_ISSOCK(sb.st_mode) || S_ISFIFO(sb.st_mode)))
		dcc_logbad(EX_UNAVAILABLE, "non-socket present at %s",
			   listen_sun.sun_path);

	/* look for a daemon already using our socket */
	listen_soc = socket(AF_UNIX, SOCK_STREAM, 0);
	if (listen_soc < 0)
		dcc_logbad(EX_OSERR, "socket(AF_UNIX): %s", ERROR_STR());
	/* unlink it only if it looks like a dead socket */
	if (0 > connect(listen_soc, (struct sockaddr *)&listen_sun,
			sizeof(listen_sun))) {
		if (errno == ECONNREFUSED || errno == ECONNRESET
		    || errno == EACCES) {
			unlink_listen_sun();
		} else if (dcc_clnt_debug > 2
			   && errno != ENOENT) {
			dcc_trace_msg("connect(old server %s): %s",
				      listen_sun.sun_path, ERROR_STR());
		}
	} else {
		/* connect() worked so the socket is alive */
		dcc_logbad(EX_UNAVAILABLE,
			   "something already or still running with socket at"
			   " %s",
			   listen_sun.sun_path);
	}
	close(listen_soc);

	listen_soc = socket(AF_UNIX, SOCK_STREAM, 0);
	if (listen_soc < 0)
		dcc_logbad(EX_OSERR, "socket(AF_UNIX): %s", ERROR_STR());
	if (0 > bind(listen_soc, (struct sockaddr *)&listen_sun,
		     sizeof(listen_sun)))
		dcc_logbad(EX_IOERR, "bind(%s) %s",
			   listen_sun.sun_path, ERROR_STR());
	if (0 > chmod(listen_sun.sun_path, 0666))
		dcc_error_msg("chmod(%s, 0666): %s",
			      listen_sun.sun_path, ERROR_STR());
}



static void
bind_tcp_listen(void)
{
	DCC_EMSG emsg;
	char host[MAXHOSTNAMELEN];
	u_int16_t port;
	DCC_SOCKU su;
	const char *cp;
	char *rhost;
	int on, error;

	emsg[0] = '\0';

	rhost = strchr(listen_addr, ',');
	if (!rhost)
		dcc_logbad(EX_USAGE, "missing port number in \"%s\"",
			   listen_addr);
	rhost = strchr(rhost+1, ',');
	if (!rhost) {
		dcc_logbad(EX_USAGE, "missing rhost in \"%s\"",
			   listen_addr);
	}
	*rhost++ = '\0';
	error = dcc_str2cidr(emsg, &raddr, &rmask, rhost, 0, 0);
	if (error <= 0) {
		dcc_logbad(EX_USAGE, "invalid rhost and mask \"%s\"",
			   rhost);
	}
	raddr_rmask = rhost;

	cp = dcc_parse_nm_port(emsg, listen_addr, DCC_GET_PORT_INVALID,
			       host, sizeof(host), &port, 0, 0, 0, 0);
	if (!cp)
		dcc_logbad(dcc_ex_code, "%s", emsg);
	if (*cp != '\0')
		dcc_logbad(EX_USAGE, "invalid IP address: \"%s\"",
			   listen_addr);

	if (host[0] == '\0' || !strcmp(host, "@")) {
		/* null or "@" means INADDR_ANY */
		dcc_mk_su(&su, use_ipv6 ? AF_INET6: AF_INET, 0, port);
	} else {
		dcc_host_lock();
		if (!dcc_get_host(host, use_ipv6, &error))
			dcc_logbad(EX_NOHOST, "%s: %s",
				   host, DCC_HSTRERROR(error));
		su = dcc_hostaddrs[0];
		*DCC_SU_PORT(&su) = port;
		dcc_host_unlock();
	}

	listen_soc = socket(su.sa.sa_family, SOCK_STREAM, 0);
	if (listen_soc < 0)
		dcc_logbad(EX_OSERR, "socket(): %s", ERROR_STR());

	on = 1;
	if (0 > setsockopt(listen_soc, SOL_SOCKET, SO_REUSEADDR,
			   &on, sizeof(on)))
		dcc_error_msg("setsockopt(listen %s, SO_REUSADDR): %s",
			      dcc_su2str(&su), ERROR_STR());

	if (0 > bind(listen_soc, &su.sa, DCC_SU_LEN(&su)))
		dcc_logbad(EX_UNAVAILABLE, "bind(%s) %s",
			   listen_addr, ERROR_STR());

	listen_family = su.sa.sa_family;
}



static u_char
set_soc(DCC_EMSG emsg, int s, int family, const char *sname)
{
	int on;

	if (0 > fcntl(s, F_SETFD, FD_CLOEXEC)) {
		dcc_pemsg(EX_IOERR, emsg,
			  "fcntl(%s, F_SETFD, FD_CLOEXEC): %s",
			  sname, ERROR_STR());
		return 0;
	}

	if (family != AF_UNIX) {
		on = 1;
		if (0 > setsockopt(s, SOL_SOCKET, SO_KEEPALIVE,
				   &on, sizeof(on))) {
			dcc_pemsg(EX_IOERR, emsg,
				  "setsockopt(%s, SO_KEEPALIVE): %s",
				  sname, ERROR_STR());
			return 0;
		}
	}

	/* use non-blocking sockets so that we can read entire lines
	 * without knowing how big they are or expecting the operating
	 * system to allow peeking at its buffers */
	if (-1 == fcntl(s, F_SETFL,
			fcntl(s, F_GETFL, 0) | O_NONBLOCK)) {
		dcc_pemsg(EX_OSERR, emsg, "fcntl(%s, O_NONBLOCK): %s",
			  sname, ERROR_STR());
		return 0;
	}

	return 1;
}



static u_char				/* 0=EOF, 1=read something */
soc_read(WORK *wp, IN_BC *bc)
{
	int fspace, len, total, i;

	if (bc->out >= bc->in)
		bc->out = bc->in = bc->base;

	fspace = &bc->base[bc->size] - bc->in;
	if (fspace < bc->size/8) {
		if (bc->out == bc->base) {
			cmn_error_msg(&wp->cw, "buffer overrun; in=%d",
				      (int)(bc->in - bc->base));
			job_exit(wp);
		}
		len = bc->in - bc->out;
		memmove(bc->base, bc->out, len);
		bc->out = bc->base;
		bc->in = bc->out+len;
		fspace = bc->size - len;
	}

	if (wp->flags & FG_WORK_LOCK)
		unlock_work();

	for (;;) {
		i = dcc_select_poll(wp->cw.emsg, *bc->socp, 1,
				    MAX_MTA_DELAY*DCC_USECS);
		if (i > 0)
			break;
		if (i < 0) {
			cmn_error_msg(&wp->cw, "%s", wp->cw.emsg);
		} else {
			cmn_error_msg(&wp->cw, "MTA read timeout");
		}
		job_exit(wp);
	}
	total = read(*bc->socp, bc->in, fspace);
	if (total < 0) {
		if (!proxy || dcc_clnt_debug)
			cmn_error_msg(&wp->cw, "read(sock): %s", ERROR_STR());
		job_exit(wp);
	}
	bc->in += total;

	if (wp->flags & FG_WORK_LOCK)
		lock_work();
	return total != 0;
}



/* ensure there is another line */
static u_char				/* 0=eof or buffer overflow */
msg_read_line(WORK *wp, IN_BC *bc)
{
	int i;
	char *p;

	for (;;) {
		p = bc->out;
		i = bc->in - p;
		if (i != 0) {
			/* look for <LF> for ASCII protocol
			 * or <CR><LF> for SMTP */
			p = memchr(p, '\n', i);
			if (p
			    && (!proxy
				|| (p > bc->out && *(p-1) == '\r'))) {
				bc->next_line = p+1;
				return 1;
			}
		}

		if (!soc_read(wp, bc))
			return 0;
	}
}



static void
buf_write_flush(WORK *wp, OUT_BC *bc)
{
	const char *buf;
	int len, i;

	len = bc->len;
	if (!len)
		return;
	bc->len = 0;

	if (*bc->socp < 0)
		dcc_logbad(EX_SOFTWARE, "attempt to write closed socket");

	buf = bc->base;
	do {
		i = dcc_select_poll(wp->cw.emsg, *bc->socp, 0,
				    MAX_MTA_DELAY*DCC_USECS);
		if (i < 0) {
			cmn_error_msg(&wp->cw, "%s", wp->cw.emsg);
			job_exit(wp);
		}
		if (i == 0) {
			cmn_error_msg(&wp->cw, "MTA write timeout");
			job_exit(wp);
		}
		i = write(*bc->socp, buf, len);
		if (i < 0) {
			if (DCC_BLOCK_ERROR())
				continue;
			cmn_error_msg(&wp->cw, "write(MTA socket,%d): %s",
				      len, ERROR_STR());
			job_exit(wp);
		}
		if (i == 0) {
			cmn_error_msg(&wp->cw, "write(MTA socket,%d)=%d",
				      len, i);
			job_exit(wp);
		}
		buf += i;
		len -= i;
	} while (len > 0);
}



static void
buf_write(WORK *wp, OUT_BC *bc, const void *buf, u_int len)
{
	u_int n;

	for (;;) {
		n = bc->size - bc->len;
		if (n > len)
			n = len;
		memcpy(&bc->base[bc->len], buf, n);
		if ((bc->len += n) >= bc->size)
			buf_write_flush(wp, bc);
		if ((len -= n) == 0)
			return;
		buf = (void *)((char *)buf + n);
	}
}



static void
tmp_write(WORK *wp, const void *buf, int len)
{
	int i;

	if (wp->tmp_fd < 0)
		return;

	i = write(wp->tmp_fd, buf, len);
	if (i != len) {
		if (i < 0)
			cmn_error_msg(&wp->cw, "write(%s,%d): %s",
				      wp->tmp_nm, len, ERROR_STR());
		else
			cmn_error_msg(&wp->cw, "write(%s,%d)=%d",
				      wp->tmp_nm, len, i);
		job_exit(wp);
	}
}



static int
tmp_read(WORK *wp, void *buf, int len)
{
	int i;

	if (wp->tmp_fd < 0)
		return 0;

	i = read(wp->tmp_fd, buf, len);
	if (i < 0) {
		cmn_error_msg(&wp->cw, "read(%s,%d): %s",
			      wp->tmp_nm, len, ERROR_STR());
		job_exit(wp);
	}
	return i;
}



/* fill MTA read buffer from temporary file */
static int
tmp_read_msg_in(WORK *wp)
{
	int i;

	/* preserving what is already in it */
	i = wp->msg_rd.in - wp->msg_rd.out;
	if (i > 0)
		memmove(wp->msg_rd.base, wp->msg_rd.out, i);
	wp->msg_rd.out = wp->msg_rd.base;
	wp->msg_rd.in = wp->msg_rd.out+i;

	wp->msg_rd.in += tmp_read(wp, wp->msg_rd.in, wp->msg_rd.size - i);
	return wp->msg_rd.in > wp->msg_rd.out;
}



static void
job_close(WORK *wp)
{
	if (wp->flags & FG_WORK_LOCK) {
		wp->flags &= ~FG_WORK_LOCK;
		unlock_work();
	}

	wp->msg_rd.socp = 0;
	if (wp->msg_wt.socp) {
		buf_write_flush(wp, &wp->msg_wt);
		wp->msg_wt.socp = 0;
	}
	wp->reply_in.socp = 0;
	if (wp->reply_out.socp) {
		buf_write_flush(wp, &wp->reply_out);
		wp->reply_out.socp = 0;
	}

	if (wp->tmp_fd >= 0) {
		if (0 > close(wp->tmp_fd))
			cmn_error_msg(&wp->cw, "close(%s): %s",
				      wp->tmp_nm, ERROR_STR());
		wp->tmp_fd = -1;
	}
	if (wp->proxy_in_soc >= 0) {
		if (0 > close(wp->proxy_in_soc))
			cmn_error_msg(&wp->cw, "close(socket): %s",
				      ERROR_STR());
		wp->proxy_in_soc = -1;
	}
	if (wp->proxy_out_soc >= 0) {
		if (0 > close(wp->proxy_out_soc))
			cmn_error_msg(&wp->cw, "close(socket): %s",
				      ERROR_STR());
		wp->proxy_out_soc = -1;
	}
	log_stop(&wp->cw);

	lock_work();
	rcpts_free(&wp->cw, 0);
	wp->fwd = work_free;
	work_free = wp;
	unlock_work();
}



static void NRATTRIB
job_exit(WORK *wp)
{
	job_close(wp);
	pthread_exit(0);
	/* mostly to suppress warning */
	dcc_logbad(EX_SOFTWARE, "pthread_exit() returned");
}



/* write headers or body data from the MTA read buffer to the MTA */
static void
mta_write(WORK *wp, char *end)
{
	int len;

	len = end - wp->msg_rd.out;
	if (!len)
		return;
	buf_write(wp, &wp->msg_wt, wp->msg_rd.out, len);
	wp->msg_rd.out = end;
}



/* copy headers from the temporary file to the MTA */
static void
hdrs_copy(WORK *wp)
{
	enum {START_HDR, SKIP_HDR, COPY_HDR} cmode;
	char *bol, *nl, *eol;
	int i;

	if (-1 == lseek(wp->tmp_fd, 0, SEEK_SET)) {
		cmn_error_msg(&wp->cw, "rewind %s: %s",
			      wp->tmp_nm, ERROR_STR());
		job_exit(wp);
	}

	cmode = START_HDR;
	wp->msg_rd.in = wp->msg_rd.out = wp->msg_rd.base;
	for (;;) {
		/* fill wp->msg_rd while keeping anything already present */
		if (!tmp_read_msg_in(wp))
			return;

		bol = wp->msg_rd.out;
		while ((i = wp->msg_rd.in - bol) > 0) {
			/* Find the end of the next line. */
			eol = memchr(bol, '\n', i);
			if (!eol) {
				/* Fill the buffer if we can't find '\n''
				 * and the buffer has room */
				if (i < wp->msg_rd.size)
					break;
				eol = wp->msg_rd.in-1;
				nl = 0;
			} else if (eol+1 >= wp->msg_rd.in) {
				/* insist on having the character after '\n'
				 * or pretend we could not find it */
				if (i < wp->msg_rd.size)
					break;
				--eol;
				nl = 0;
			} else {
				nl = eol+1;
			}

			if (cmode == START_HDR) {
				/* We are at start of a header or the body.
				 * Quit before line of "\n" or "\r\n" */
				if (eol == bol
				    || (eol == bol+1 && *bol == '\r')) {
					/* write any preceding lines */
					mta_write(wp, bol);
					return;
				}

				/* Look for our header
				 * Assume the buffer is larger than the
				 * largest possible X-DCC header. */
				if (bol[wp->cw.xhdr_len] == ':'
				    && !strncasecmp(bol, wp->cw.xhdr,
						    wp->cw.xhdr_len)
				    && chghdr == SETHDR) {
					/* Found it and we want to skip it.
					 * Copy any preceding headers. */
					mta_write(wp, bol);
					cmode = SKIP_HDR;
				} else {
					cmode = COPY_HDR;
				}
			}
			if (cmode == SKIP_HDR)
				wp->msg_rd.out = eol+1;

			/* Check the character after '\n' for
			 * whitespace indicating continuation */
			if (nl && *nl  != ' ' && *nl != '\t') {
				cmode = START_HDR;
				/* deal with transparency */
				if (proxy && *nl == '.')
					*eol-- = '.';
			}

			bol = eol+1;
		}

		mta_write(wp, bol);
	}
}



static void
body_copy(WORK *wp)
{
	u_char seen_crlf;
	char *p;

	hdrs_copy(wp);

	if (wp->cw.header.buf[0] != '\0'
	    && chghdr != NOHDR) {
		/* write X-DCC */
		buf_write(wp, &wp->msg_wt, wp->cw.header.buf,
			  wp->cw.header.used);
		if (wp->cr_hdrs >= wp->total_hdrs/2) {
			/* end the X-DCC header with "\r\n" if at least
			 * half of the header lines ended that way */
			buf_write(wp, &wp->msg_wt, "\r\n", 2);
		} else {
			/* otherwise use only "\n" */
			buf_write(wp, &wp->msg_wt, "\n", 1);
		}
	}

	/* copy body */
	seen_crlf = 2;
	do {
		p = wp->msg_rd.out;
		while (p < wp->msg_rd.in) {
			if (seen_crlf == 1) {
				seen_crlf = (*p++ == '\n') ? 2 : 0;
				continue;
			}
			if (seen_crlf == 2) {
				seen_crlf = 0;
				if (*p == '.' && proxy) {
					mta_write(wp, p);
					buf_write(wp, &wp->msg_wt, ".", 1);
				}
			}

			if (!proxy)
				break;
			p = memchr(p, '\r', wp->msg_rd.in-p);
			if (!p)
				break;
			seen_crlf = 1;
			++p;
		}
		mta_write(wp, wp->msg_rd.in);
	} while (tmp_read_msg_in(wp));
}



static void
close_listen_soc(void)
{
	if (listen_soc < 0)
		return;

	unlink_listen_sun();
	if (0 > close(listen_soc))
		dcc_error_msg("close(main socket): %s",
			      ERROR_STR());
	listen_soc = -1;
}



/* watch for fatal signals */
static void
sigterm(int sig)
{
	stopping = sig;
	unlink_listen_sun();
	dcc_clnt_stop_resolve();
}



void
user_reject(CMN_WORK *cwp, UATTRIB RCPT_ST *rcpt_st,
	    USER_REJECT_TYPE rej_type)
{
	if (rej_type == USER_REJECT_RCPT) {
		++totals.tgts_discarded;
		--cwp->reject_tgts;
		return;
	}
}



static void
get_helo(WORK *wp, const char *vp, int i)
{
	if (i < DCC_HELO_MAX-1) {
		memcpy(wp->cw.helo, vp, i);
		wp->cw.helo[i] = '\0';
	} else {
		i = DCC_HELO_MAX-1;
		memcpy(wp->cw.helo, vp, i);
		strcpy(&wp->msg_rd.out[DCC_HELO_MAX-ISZ(DCC_HELO_CONT)],
		       DCC_HELO_CONT);
	}
}



/* Get the next header field
 *      The line is copied to the temporary file and then null terminated
 *      in the buffer */
static u_char				/* 1=have one, 0=end of headers */
get_hdr(WORK *wp)
{
	int tf_len;			/* header bytes written to temp file */
	int hdr_len;			/* total header length */
	int nlen;			/* length of next chunk of header */
	char *np;			/* next byte after field */
	char *p;
	u_char at_bol;			/* 1=at start of a line of header */

	tf_len = 0;
	hdr_len = 0;
	at_bol = 1;
	for (;;) {
		/* get another line of the header */
		np = wp->msg_rd.out + hdr_len;
		nlen = wp->msg_rd.in - np;
		if (nlen <= 1) {
			/* we do not have enough data */
read_more:;
			if (hdr_len > DCC_HDR_CK_MAX) {
				/* we already have more than DCC_HDR_CK_MAX
				 * bytes of the header.
				 * Keep only DCC_HDR_CK_MAX bytes of it
				 * in the buffer.  We will eventually
				 * return only the first DCC_HDR_CK_MAX
				 * bytes and an arbitrary part of the tail
				 * to our caller. */
				if (tf_len < hdr_len)
					tmp_write(wp, wp->msg_rd.out+tf_len,
						  hdr_len - tf_len);
				/* discard all but the first DCC_HDR_CK_MAX
				 * bytes of the header */
				hdr_len = DCC_HDR_CK_MAX;
				tf_len = DCC_HDR_CK_MAX;
				if (nlen != 0)
					wp->msg_rd.out[DCC_HDR_CK_MAX] = *np;
				wp->msg_rd.in = (wp->msg_rd.out + DCC_HDR_CK_MAX
						 + nlen);
			}
			/* Get more data, possibly sliding what we already
			 * have down in the buffer.  That would move
			 * wp->msg_rd.in to wp->msg_rd.base. */
			if (!soc_read(wp, &wp->msg_rd)) {
				/* EOF implies the message body including the
				 * separating blank line is missing.
				 * Fake newlines until we get out of here
				 * so that we will log whatever we got */
				wp->flags |= FG_MISSING_BODY;
				if (wp->cr_hdrs >= wp->total_hdrs/2)
					*wp->msg_rd.in++ = '\r';
				*wp->msg_rd.in++ = '\n';
			}
			continue;
		}

		if (at_bol) {
			/* deal with SMTP transparency */
			if (*np == '.' && proxy) {
				if (nlen < 3)
					goto read_more;
				if (np[1] != '\r' || np[2] != '\n') {
					memmove(np, np+1, --nlen);
				} else if (hdr_len == 0) {
					/* stop before end of message */
					return 0;
				}
			}

			/* stop before the next line if it is
			 * not a continuation of the current header */
			if (hdr_len != 0
			    && *np != ' '
			    && *np != '\t') {
				if (tf_len < hdr_len)
					tmp_write(wp, wp->msg_rd.out+tf_len,
						  hdr_len - tf_len);
				wp->msg_rd.out[hdr_len-1] = '\0';
				if (hdr_len > 1
				    && wp->msg_rd.out[hdr_len-2] == '\r')
					++wp->cr_hdrs;
				wp->msg_rd.next_line = &wp->msg_rd.out[hdr_len];
				return 1;
			}
		}

		/* find the end of the next line */
		p = memchr(np, '\n', nlen);
		if (p) {
			hdr_len += ++p - np;
			if (hdr_len == 1
			    || (hdr_len == 2 && *wp->msg_rd.out == '\r'))
				return 0;   /* quit at the end of headers */
			at_bol = 1;
		} else {
			hdr_len += nlen;
			at_bol = 0;
		}
	}
}



static void
get_hdrs(WORK *wp)
{
	char *p, *p2;
	int i;

	for (;;) {
		/* stop at the separator between the body and headers */
		if (!get_hdr(wp))
			break;
		++wp->total_hdrs;

#define GET_HDR_CK(h,t) {						\
				if (!CSTRCMP(wp->msg_rd.out, h)) {		\
				dcc_get_cks(&wp->cw.cks, DCC_CK_##t,    \
					    &wp->msg_rd.out[STRZ(h)], 1); \
				wp->flags |= FG_SEEN_HDR;		\
				wp->msg_rd.out = wp->msg_rd.next_line;  \
				continue;}}
		GET_HDR_CK(DCC_XHDR_TYPE_FROM":", FROM);
		GET_HDR_CK(DCC_XHDR_TYPE_MESSAGE_ID":", MESSAGE_ID);
#undef GET_HDR_CK

		if (!CSTRCMP(wp->msg_rd.out, "Return-Path:")) {
			if (wp->cw.cks.sums[DCC_CK_ENV_FROM].type
			    == DCC_CK_INVALID) {
				BUFCPY(wp->env_from,
				       &wp->msg_rd.out[STRZ("Return-Path:")]);
				dcc_get_cks(&wp->cw.cks, DCC_CK_ENV_FROM,
					    wp->env_from, 1);
			}
			wp->flags |= FG_SEEN_HDR;
			wp->msg_rd.out = wp->msg_rd.next_line;
			continue;
		}

		/* notice UNIX From_ line */
		if (!(wp->flags & FG_SEEN_HDR)
		    && !strncmp(wp->msg_rd.out, "From ", STRZ("From "))) {
			p = &wp->msg_rd.out[STRZ("From ")];
			p += strspn(p, " ");
			p2 = strchr(p, ' ');
			if (p2 != 0) {
				if (wp->cw.cks.sums[DCC_CK_ENV_FROM].type
				    == DCC_CK_INVALID) {
					if (p2 > p+sizeof(wp->env_from))
					    p2 = p+sizeof(wp->env_from);
					memcpy(wp->env_from, p, p2-p);
					wp->env_from[p2-p] = '\0';
				}
				wp->flags |= FG_SEEN_HDR;
				wp->msg_rd.out = wp->msg_rd.next_line;
				continue;
			}
		}

		if (!CSTRCMP(wp->msg_rd.out, DCC_XHDR_TYPE_RECEIVED":")) {
			p = &wp->msg_rd.out[STRZ(DCC_XHDR_TYPE_RECEIVED":")];

			/* compute checksum of the last Received: header */
			dcc_get_cks(&wp->cw.cks, DCC_CK_RECEIVED, p, 1);

			/* pick IP address out of first Received: header */
			if ((wp->flags & FG_CK_RCVD)
			    && 0 != (p2 = strchr(p, '('))
			    && 0 != (p = strchr(++p2, '['))
			    && (i = strspn(++p, ".:abcdefABCDEF0123456789")) > 6
			    && i < INET6_ADDRSTRLEN
			    && p[i] == ']') {
				p[i] = '\0';
				if (dcc_get_str_ip_ck(&wp->cw.cks, p,
						      &wp->cw.clnt_addr)) {
					if (!DCC_INET_NTOP(AF_INET6,
							&wp->cw.clnt_addr,
							wp->cw.clnt_str,
						sizeof(wp->cw.clnt_str)))
					    strcpy(wp->cw.clnt_str, p);

					wp->cw.rcpt_st_flags
					    &= ~RCPT_ST_NO_CLNT_ADDR;

					i = p-p2-1;
					if (i > ISZ(wp->cw.clnt_name)-1)
					    i = ISZ(wp->cw.clnt_name)-1;
					while (i > 0
					       && (p2[i-1] == ' '
						   || p2[i-1] == '\t'))
					    --i;
					if (i > 0) {
					    memcpy(wp->cw.clnt_name, p2, i);
					    wp->cw.clnt_name[i] = '\0';
					} else {
					    snprintf(wp->cw.clnt_name,
						     sizeof(wp->cw.clnt_name),
						     "[%s]", p);
					}
				}
			}
			wp->flags &= ~FG_CK_RCVD;
			wp->flags |= FG_SEEN_HDR;
			wp->msg_rd.out = wp->msg_rd.next_line;
			continue;
		}

		/* Notice MIME multipart boundary definitions */
		dcc_ck_mime_hdr(&wp->cw.cks, wp->msg_rd.out, 0);

		if (dcc_ck_get_sub(&wp->cw.cks, wp->msg_rd.out, 0))
			wp->flags |= FG_SEEN_HDR;

		/* notice any sort of header */
		if (!(wp->flags & FG_SEEN_HDR)) {
			for (p = wp->msg_rd.out; ; ++p) {
				if (*p == ':') {
					wp->flags |= FG_SEEN_HDR;
					break;
				}
				if (*p <= ' ' || *p >= 0x7f)
					break;
			}
		}

		wp->msg_rd.out = wp->msg_rd.next_line;
	}

	/* Create a checksum for a null Message-ID header if there
	 * was no Message-ID header.  */
	if (wp->cw.cks.sums[DCC_CK_MESSAGE_ID].type != DCC_CK_MESSAGE_ID)
		dcc_get_cks(&wp->cw.cks, DCC_CK_MESSAGE_ID, "", 0);
}



static u_char				/* 0=temporary failure, 1=ok */
get_body(WORK *wp)
{
	RCPT_ST *rcpt_st;
	char *mail_host, *p, *p2, *lim;
	char buf[1024];
	u_char bol;
	int buflen, i;

	/* We must make a copy of the entire message in a temporary file
	 * if the MTA wants a copy with the X-DCC header added
	 * Copy the headers to a temporary file because the official
	 * log file needs the SMTP client IP address and envelope information
	 * before the header lines.  The log file needs all of the header
	 * lines including stray X-DCC lines, but those lines must be removed
	 * from the output file. */
	wp->tmp_fd = dcc_mkstemp(wp->cw.emsg, wp->tmp_nm, sizeof(wp->tmp_nm),
				 tmpdir, "/tmp.", 1, 0, 0);
	if (wp->tmp_fd < 0) {
		dcc_error_msg("%s", wp->cw.emsg);
		job_close(wp);
	}

	/* open the connection to the nearest DCC server
	 * and figure out our X-DCC header */
	if (!ck_dcc_ctxt(&wp->cw)) {
		/* failed to create context */
		wp->cw.dcc_ctxt_sn = 0;
		return 0;
	}

	/* Get ready for the body checksums before the headers so that
	 * we can notice the MIME separator */
	dcc_ck_body_init(&wp->cw.cks);
	dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, wp->cw.id, max_work);

	get_hdrs(wp);

	/* log IP address and so forth that we may have collected from
	 * the headers or Postfix XFORWARD ESTMP extension */
	if (wp->cw.rcpt_st_flags & RCPT_ST_NO_CLNT_ADDR)
		wp->cw.cks.sums[DCC_CK_IP].type = DCC_CK_INVALID;
	if (wp->cw.clnt_name[0] != '\0'
	    || wp->cw.cks.sums[DCC_CK_IP].type != DCC_CK_INVALID) {
		log_print(&wp->cw, DCC_XHDR_TYPE_IP": %s %s\n",
			  wp->cw.clnt_name, wp->cw.clnt_str);
	}

	/* log HELO value if we have it
	 * make checksum of it or of null string if we don't */
	if (wp->cw.helo[0] != '\0')
		log_print(&wp->cw, "HELO: %s\n", wp->cw.helo);
	dcc_ck_get_sub(&wp->cw.cks, "helo", wp->cw.helo);

	if (wp->env_from[0] == '\0') {
		mail_host = 0;
	} else {
		LOG_CAPTION(wp, DCC_XHDR_TYPE_ENV_FROM": ");
		log_write(&wp->cw, wp->env_from, 0);
		dcc_get_cks(&wp->cw.cks, DCC_CK_ENV_FROM, wp->env_from, 1);

		LOG_CAPTION(wp, "  mail_host=");
		mail_host = strchr(wp->env_from, '@');
		if (mail_host) {
			++mail_host;
			p2 = strchr(mail_host, '>');
			if (p2)
				*p2 = '\0';
			if (strchr(mail_host, ';') || strchr(mail_host, '@')
			    || strchr(mail_host, ','))
			mail_host = 0;
		}
		if (mail_host) {
			log_write(&wp->cw, mail_host, 0);
			dcc_ck_get_sub(&wp->cw.cks, "mail_host", mail_host);
		}
		LOG_EOL(wp);
	}
	wp->cw.log_pos_to_first = log_lseek(&wp->cw, SEEK_END);
	wp->cw.log_pos_to_end = wp->cw.log_pos_to_first;
	for (rcpt_st = wp->cw.rcpt_st_first; rcpt_st; rcpt_st = rcpt_st->fwd) {
		rcpt_st->log_pos_to = wp->cw.log_pos_to_end;
		LOG_CAPTION(wp, DCC_XHDR_TYPE_ENV_TO": ");
		log_write(&wp->cw, rcpt_st->env_to, 0);
		LOG_CAPTION(wp, "  addr=");
		log_write(&wp->cw, rcpt_st->user, 0);
		LOG_CAPTION(wp, "  dir=");
		log_write(&wp->cw, rcpt_st->dir, 0);
		LOG_EOL(wp);
		wp->cw.log_pos_to_end = log_lseek(&wp->cw, SEEK_END);
	}

	/* log the blank line between the header and the body */
	LOG_EOL(wp);

	/* do not bother with DNS blacklist checks if already have the answer */
	if (wp->cw.cks.dnsbl) {
		if ((wp->cw.honor & DCC_HONOR_MTA_ISSPAM)
		    && wp->cw.cks.dnsbl->hit == DNSBL_HIT_NONE) {
			wp->cw.cks.dnsbl->hit = DNSBL_HIT_MTA;
		} else {
			/* check DNS blacklists for STMP client
			 * and envelope sender */
			if (!(wp->cw.rcpt_st_flags & RCPT_ST_NO_CLNT_ADDR))
				dcc_sender_dnsbl(wp->cw.cks.dnsbl,
						 &wp->cw.clnt_addr);
			if (mail_host)
				dcc_mail_host_dnsbl(wp->cw.cks.dnsbl,
						    mail_host);
		}
	}

	/* copy headers from tmp file to log file */
	if (wp->cw.log_fd >= 0) {
		if (0 > lseek(wp->tmp_fd, 0, SEEK_SET)) {
			cmn_error_msg(&wp->cw, "rewind %s: %s",
				      wp->tmp_nm, ERROR_STR());
			job_exit(wp);
		}
		for (;;) {
			buflen = tmp_read(wp, buf, sizeof(buf));
			if (buflen <= 0)
				break;
			log_write(&wp->cw, buf, buflen);
		}
	}
	if (!(wp->flags & FG_MTA_BODY)) {
		if (0 > close(wp->tmp_fd))
			cmn_error_msg(&wp->cw, "close(%s): %s",
				      wp->tmp_nm, ERROR_STR());
		wp->tmp_fd = -1;
	}

	if (wp->flags & FG_MISSING_BODY)
		cmn_error_msg(&wp->cw, "missing message body");

	/* collect the body */
	bol = 1;
	for (;;) {
		buflen = wp->msg_rd.in - wp->msg_rd.out;
		if (buflen <= 0) {
			if (!soc_read(wp, &wp->msg_rd))
				break;
			buflen = wp->msg_rd.in - wp->msg_rd.out;
		}

		/* deal with SMTP transparency and detect end of message */
		if (proxy) {
			if (bol) {
				/* We are at the beginning of a line.
				 * There will always be at least 3 more bytes
				 * in the message, ".\r\n" */
				if (buflen < 3) {
					soc_read(wp, &wp->msg_rd);
					buflen = wp->msg_rd.in - wp->msg_rd.out;
				}
				/* Whether a '.' is the end of the data
				 * or an escaped '.', we will discard it. */
				if (*wp->msg_rd.out == '.'
				    && buflen >= 1) {
					++wp->msg_rd.out;
					--buflen;
					/* if it is followed by "\r\n", then
					 * we have the end of the message */
					if (buflen >= 2
					    && wp->msg_rd.out[0] == '\r'
					    && wp->msg_rd.out[1] == '\n') {
					    wp->msg_rd.out += 2;
					    break;
					}
				}
				/* we are still at the beginning of a line */
			}

			/* check all of the lines in the buffer */
			p = wp->msg_rd.out;
			for (;;) {
				i = p - wp->msg_rd.out;
				p = memchr(p, '\r', buflen-i);
				if (!p) {
					/* We have checked all of the buffer.
					 * It does not end near a line end
					 * So log the block of body lines. */
					bol = 0;
					break;
				}
				/* we have found '\r'.  Maybe it will
				 * be followed by '\n' */
				i = ++p - wp->msg_rd.out;
				if (i+2 >= buflen) {
					/* '\r' ends or almost ends the buffer.
					 * Process everything up to the '\r'
					 * and try again later */
					bol = 0;
					buflen = i-1;
					if (buflen > 0) {
					    /* buffer ends with "\rX"
					     * but contains more
					     * process up to the '\r' */
					    break;
					}
					/* buffer consists of "\rX" */
					if (!soc_read(wp, &wp->msg_rd))
					    break;
					p = wp->msg_rd.out;
					buflen = wp->msg_rd.in - p;

				} else if (*p == '\n') {
					/* We have "\r\n"
					 * If "\r\n" is followed by '.',
					 * then process up to the '.' */
					if (*++p == '.') {
					    buflen = i+1;
					    bol = 1;
					    break;
					}
				}
			}
		}

		/* Log the body block
		 * avoid filling the disk by truncating the body */
		if (wp->cw.log_fd >= 0) {
#if MAX_LOG_SIZE > 0
			i = MAX_LOG_SIZE - wp->cw.log_size;
			if (i >= 0) {
				if (i >= buflen) {
					log_write(&wp->cw,
						  wp->msg_rd.out, buflen);
				} else {
					p = wp->msg_rd.out+i;
					lim = wp->msg_rd.out;
					p = lim+i;
					if (i > 80)
					    lim += i-80;
					while (--p > lim) {
					    if (*p == '\n') {
						i = p-wp->msg_rd.out+1;
						break;
					    }
					}
					log_write(&wp->cw, wp->msg_rd.out, i);
					if (wp->msg_rd.out[i-1] != '\n')
					    LOG_EOL(wp);
					LOG_CAPTION(wp, DCC_LOG_TRN_MSG_CR);
					wp->cw.log_size = MAX_LOG_SIZE+1;
				}
			}
#else
			log_write(&wp->cw, wp->msg_rd.out, buflen);
#endif
		}

		if (wp->flags & FG_MTA_BODY)
			tmp_write(wp, wp->msg_rd.out, buflen);

		dcc_ck_body(&wp->cw.cks, wp->msg_rd.out, buflen);
		wp->msg_rd.out += buflen;
	}
	dcc_ck_body_fin(&wp->cw.cks);

	LOG_CAPTION(wp, DCC_LOG_MSG_SEP);

	if (wp->cw.honor & DCC_HONOR_MTA_ISSPAM)
		LOG_CAPTION(wp, "MTA-->spam\n");

	/* check the grey and white lists */
	cmn_ask_white(&wp->cw,
		      (wp->flags & FG_MTA_GREY_QUERY) != 0);

	LOG_CAPTION(wp, "\n");
	wp->cw.header.buf[0] = '\0';
	wp->cw.header.used = 0;
	if (wp->cw.tgts != wp->cw.white_tgts || wp->cw.tgts == 0) {
		/* Report to the DCC and add our header if allowed.
		 * After serious errors, act as if DCC server said not-spam */
		if (!cmn_ask_dcc(&wp->cw,
				 (wp->flags & FG_MTA_DCC_QUERY) != 0)
		    && try_extra_hard)
			return 0;

	} else {
		/* if not ok to ask, remove our X-DCC header */
		wp->cw.cks.flags &= ~DCC_CKS_HAVE_SUM;
		/* reject and/or log it if the target count is high enough */
		dcc_honor_log_cnts(&wp->cw.honor, &wp->cw.cks,
				   ((wp->cw.honor & DCC_HONOR_LOCAL_ISSPAM)
				    ? DCC_TGTS_TOO_MANY : wp->cw.tgts));
	}

	totals.tgts += wp->cw.tgts;
	users_process(&wp->cw);

	/* deliver it if all remaining targets want it */
	totals.tgts_rejected += wp->cw.reject_tgts;

	return 1;
}



/* ensure there is another line, trim its terminal "\r\n",
 *      and add a terminal '\0'
 * This cannot be used with the proxy code because it often needs to
 *	send the original buffer downstream. */
static int				/* # of bytes available */
ascii_read_line(WORK *wp)
{
	char *p;

	if (!msg_read_line(wp, &wp->msg_rd)) {
		cmn_error_msg(&wp->cw, "truncated request");
		job_exit(wp);
	}

	p = wp->msg_rd.next_line-1;
	for (;;) {
		*p-- = '\0';
		if (p < wp->msg_rd.out)
			return 0;
		if (*p != '\r')
			return p+1 - wp->msg_rd.out;
	}
}



/* Look for a string from the MTA
 *      while ignoring case and skipping whitespace
 *      The next line must already be in the buffer */
static u_char				/* 1=matched it */
ascii_opt_str(WORK *wp, const char *st, int stlen)
{
	/* skip initial white space */
	while (wp->msg_rd.out < wp->msg_rd.next_line
	       && (*wp->msg_rd.out == '\t' || *wp->msg_rd.out == ' '))
		++wp->msg_rd.out;

	if (stlen <= wp->msg_rd.next_line - wp->msg_rd.out
	    && !strncasecmp(wp->msg_rd.out, st, stlen)) {
		if (wp->msg_rd.out[stlen] == '\0') {
			wp->msg_rd.out += stlen;
			return 1;
		}

		/* skip trailing whitespace */
		if (wp->msg_rd.out[stlen] == '\t'
		    || wp->msg_rd.out[stlen] == ' ') {
			do {
				++stlen;
			} while (wp->msg_rd.out[stlen] == '\t'
				 || wp->msg_rd.out[stlen] == ' ');
			wp->msg_rd.out += stlen;
			return 1;
		}
	}

	return 0;
}



static u_char				/* 0=bad recipient */
check_addr(WORK *wp,
	   const char **addrp, int *addr_lenp,
	   const char **userp, int *user_lenp)
{
	USER_DOMAIN *udom;
	const char *addr, *cp;
	int addr_len, i;

	addr = *addrp;
	addr_len = *addr_lenp;
	if (*addr == '<' && addr_len >= 2 && addr[addr_len-1] == '>') {
		++addr;
		addr_len -= 2;
		if (addr_len == 0) {
			if (wp->flags & FG_WORK_LOCK)
				unlock_work();
			cmn_error_msg(&wp->cw, "null recipient address");
			if (wp->flags & FG_WORK_LOCK)
				lock_work();
			return 0;
		}
	}

	/* strip source route from recipient */
	while (addr_len != 0
	       && (cp = memchr(addr, ',', addr_len)) != 0) {
		++cp;
		addr_len -= cp-addr;
		addr = cp;
		*addr_lenp = addr_len;
		*addrp = addr;
	}

	if (addr_len >= RCTP_MAXNAME) {
		if (wp->flags & FG_WORK_LOCK)
			unlock_work();
		cmn_error_msg(&wp->cw, "recipient \"%s\" is too long", addr);
		if (wp->flags & FG_WORK_LOCK)
			lock_work();
		return 0;
	}
	if (addr_len <= 0) {
		if (wp->flags & FG_WORK_LOCK)
			unlock_work();
		cmn_error_msg(&wp->cw, "null recipient");
		if (wp->flags & FG_WORK_LOCK)
			lock_work();
		return 0;
	}

	/* use the list of local domain names to guess a user name */
	if (!*user_lenp) {
		/* take the whole recipient name if it does not
		 * include a domain name */
		if (!memchr(addr, '@', addr_len)) {
			*userp = addr;
			*user_lenp = addr_len;
			return 1;
		}

		for (udom = user_domains; udom; udom = udom->fwd) {
			i = addr_len - udom->len;
			if (i > 0
			    && !strncasecmp(addr+i, udom->nm,
					    udom->len)) {
				if (*udom->nm == '@'
				    || *udom->nm == '.') {
					;   /* got it */
				} else {
					/* match must start at '.' or '@'
					 * if target did not start with
					 * '.' or '@' then the name must
					 * end with '.' or '@' */
					if (i-- < 2
					    || (addr[i] != '.'
						&& addr[i] != '@'))
					    continue;
				}
				/* we have something like user
				 * or user@sub
				 * from user@sub.domain.com */
				*userp = addr;
				*user_lenp = i;
				return 1;
			}
		}
	}

	return 1;
}



/* The mutex must be locked */
static void
set_rcpt(WORK *wp,
	 const char *rcpt, int rcpt_len,
	 const char *user, int user_len)
{
	RCPT_ST *rcpt_st;

	rcpt_st = rcpt_st_alloc(&wp->cw, 0);
	if (!rcpt_st)
		job_exit(wp);

	++wp->cw.tgts;
	memcpy(rcpt_st->env_to, rcpt, rcpt_len);
	rcpt_st->env_to[rcpt_len] = '\0';
	if (user_len)
		memcpy(rcpt_st->user, user, user_len);
	rcpt_st->user[user_len] = '\0';

	rcpt_st->dir[0] = '\0';
	if (user_len != 0
	    && !get_user_dir(rcpt_st, user, user_len, 0, 0))
		cmn_trace_msg(&wp->cw, "%s", wp->cw.emsg);
}



/* read lines of pairs if (env_To, username) values from the MTA */
static void
get_ascii_rcpts(WORK *wp)
{
	const char *addr, *user, *user_end;
	char c;
	int addr_len, user_len;

	lock_work();
	wp->flags |= FG_WORK_LOCK;
	for (;;) {
		ascii_read_line(wp);

		/* stop after the empty line */
		addr = wp->msg_rd.out;
		if (*addr == '\0') {
			wp->msg_rd.out = wp->msg_rd.next_line;
			break;
		}

		user_end = wp->msg_rd.next_line;
		while (user_end > addr
		       && ((c = *(user_end-1)) == ' ' || c == '\t'))
			--user_end;

		/* bytes up to '\r' are the env_To value
		 * and bytes after are the local recipient */
		user = strchr(addr, '\r');
		if (!user) {
			addr_len = user_end - wp->msg_rd.out;
			user = user_end;
		} else {
			addr_len  = user - addr;
			++user;
		}

		user_len = user_end - user;
		if (!check_addr(wp, &addr, &addr_len, &user, &user_len))
			job_exit(wp);
		set_rcpt(wp, addr, addr_len, user, user_end - user);

		wp->msg_rd.out = wp->msg_rd.next_line;
	}
	unlock_work();
	wp->flags &= ~FG_WORK_LOCK;
}



/* We are finished with one SMTP message with the ASCII protocol
 *      Send the result to the MTA and end this thread */
static void NRATTRIB
ascii_done(WORK *wp, char result_char, const char *result_str)
{
	RCPT_ST *rcpt_st;
	char *p;

	LOG_CAPTION(wp, "result: ");
	log_write(&wp->cw, result_str, 0);
	LOG_EOL(wp);

	/* tell MTA the overall result */
	p = wp->msg_rd.base;
	*p++ = result_char;
	*p++ = '\n';

	/* output list of recipients */
	for (rcpt_st = wp->cw.rcpt_st_first; rcpt_st; rcpt_st = rcpt_st->fwd) {
		if (p >= &wp->msg_rd.base[wp->msg_rd.size-1]) {
			buf_write(wp, &wp->msg_wt,
				  wp->msg_rd.base, wp->msg_rd.size-1);
			p = wp->msg_rd.base;
		}
		if (result_char == DCCIF_RESULT_GREY) {
			*p++ = DCCIF_RCPT_GREY;
		} else if (rcpt_st->flags & RCPT_ST_BLACK ) {
			*p++ = DCCIF_RCPT_REJECT;
			/* tell greylist to restore embargo for those targets
			 * that believe the message was spam and did not
			 * white- or blacklist it */
			if (rcpt_st->grey_result != DCC_GREY_ASK_OFF
			    && rcpt_st->grey_result != DCC_GREY_ASK_FAIL
			    && !dcc_grey_spam(wp->cw.emsg, wp->cw.dcc_ctxt,
					      &wp->cw.cks, rcpt_st->triple_sum))
				cmn_error_msg(&wp->cw, "%s", wp->cw.emsg);
		} else {
			*p++ = DCCIF_RCPT_ACCEPT;
		}
	}
	*p++ = '\n';
	buf_write(wp, &wp->msg_wt, wp->msg_rd.base, p-wp->msg_rd.base);

	/* send the body from the temporary file to the MTA */
	if (wp->flags & FG_MTA_BODY) {
		body_copy(wp);

	} else if (wp->flags & FG_MTA_HEADER) {
		/* MTA wants only the header, if we have it */
		if (wp->cw.header.used != 0)
			buf_write(wp, &wp->msg_wt, wp->cw.header.buf,
				       wp->cw.header.used);
		buf_write(wp, &wp->msg_wt, "\n", 1);
	}

	job_exit(wp);
}



/* use the dccifd ASCII protocol to talk to an MTA */
static void NRATTRIB
ascii_job(WORK *wp)
{
	char *p;
	int i;

	wp->msg_rd.base = wp->buf1;
	wp->msg_rd.size = sizeof(wp->buf1);
	wp->msg_rd.socp = &wp->proxy_in_soc;

	wp->msg_wt.base = wp->buf2;
	wp->msg_wt.size = sizeof(wp->buf2);
	wp->msg_wt.socp = &wp->proxy_in_soc;

	/* get any options */
	ascii_read_line(wp);
	while (*wp->msg_rd.out != '\0') {
		if (ascii_opt_str(wp, DCCIF_OPT_SPAM,
				sizeof(DCCIF_OPT_SPAM)-1)) {
			wp->cw.honor |= DCC_HONOR_MTA_ISSPAM;
			continue;
		}
		if (ascii_opt_str(wp, DCCIF_OPT_BODY,
				sizeof(DCCIF_OPT_BODY)-1)) {
			wp->flags |= FG_MTA_BODY;
			continue;
		}
		if (ascii_opt_str(wp, DCCIF_OPT_HEADER,
				sizeof(DCCIF_OPT_HEADER)-1)) {
			wp->flags |= FG_MTA_HEADER;
			continue;
		}
		if (ascii_opt_str(wp, DCCIF_OPT_QUERY,
				sizeof(DCCIF_OPT_QUERY)-1)) {
			wp->flags |= FG_MTA_DCC_QUERY;
			continue;
		}
		if (ascii_opt_str(wp, DCCIF_OPT_GREY_QUERY,
				sizeof(DCCIF_OPT_GREY_QUERY)-1)) {
			wp->flags |= FG_MTA_GREY_QUERY;
			continue;
		}
		if (ascii_opt_str(wp, DCCIF_OPT_NO_REJECT,
				  sizeof(DCCIF_OPT_NO_REJECT)-1)) {
			wp->cw.action = CMN_IGNORE;
			continue;
		}

		cmn_error_msg(&wp->cw, "unrecognized option value: \"%s\"",
			      wp->msg_rd.out);
		job_exit(wp);
	}
	if ((wp->cw.honor & DCC_HONOR_MTA_ISSPAM)
	    && (wp->flags & FG_MTA_DCC_QUERY)) {
		cmn_error_msg(&wp->cw, DCCIF_OPT_SPAM" and "DCCIF_OPT_QUERY
			      " are incompatible");
		wp->cw.honor &= ~DCC_HONOR_MTA_ISSPAM;
	}
	if ((wp->cw.honor & DCC_HONOR_MTA_ISSPAM)
	    && (wp->flags & FG_MTA_GREY_QUERY)) {
		cmn_error_msg(&wp->cw, DCCIF_OPT_SPAM" and "DCCIF_OPT_GREY_QUERY
			      " are incompatible");
		wp->cw.honor &= ~DCC_HONOR_MTA_ISSPAM;
	}
	wp->msg_rd.out = wp->msg_rd.next_line;

	/* get the SMTP client IP address and host name */
	i = ascii_read_line(wp);
	if (i == 0) {
		wp->cw.clnt_name[0] = '\0';
		wp->cw.clnt_str[0] = '\0';
		wp->flags |= FG_CK_RCVD;    /* try first Received header */
		wp->cw.rcpt_st_flags |= RCPT_ST_NO_CLNT_ADDR;
	} else {
		/* the host name follows the IP address */
		p = strchr(wp->msg_rd.out, '\r');
		if (p) {
			*p++ = '\0';
			BUFCPY(wp->cw.clnt_name, p);
		} else {
			wp->cw.clnt_name[0] = '\0';
		}
		/* convert ASCCI representation of IP address to a
		 * canonical form and to a checksum */
		if (!dcc_get_str_ip_ck(&wp->cw.cks, wp->msg_rd.out,
				       &wp->cw.clnt_addr)) {
			cmn_error_msg(&wp->cw, "unrecognized IP address \"%s\"",
				      wp->msg_rd.out);
			wp->cw.rcpt_st_flags |= RCPT_ST_NO_CLNT_ADDR;
		} else {
			/* convert IPv6 address to a canonical string */
			if (!DCC_INET_NTOP(AF_INET6, &wp->cw.clnt_addr,
					   wp->cw.clnt_str,
					   sizeof(wp->cw.clnt_str)))
				BUFCPY(wp->cw.clnt_str, wp->msg_rd.out);
		}
	}
	wp->msg_rd.out = wp->msg_rd.next_line;

	/* get the HELO value */
	i = ascii_read_line(wp);
	get_helo(wp, wp->msg_rd.out, i);
	wp->msg_rd.out = wp->msg_rd.next_line;

	/* get the envelope Mail_From value */
	i = ascii_read_line(wp);
	if (i > ISZ(wp->env_from)-1) {
		i = ISZ(wp->env_from)-1;
		wp->msg_rd.out[ISZ(wp->env_from)] = '\0';
	}
	memcpy(wp->env_from, wp->msg_rd.out, i+1);
	if (wp->env_from[0] != '\0')
		dcc_get_cks(&wp->cw.cks, DCC_CK_ENV_FROM,
			    wp->env_from, 1);
	wp->msg_rd.out = wp->msg_rd.next_line;

	/* get the list of recipients from the MTA */
	get_ascii_rcpts(wp);

	if (!get_body(wp))
		ascii_done(wp, DCCIF_RESULT_TEMP, "temporary failure");

	if ((wp->cw.reject_tgts != 0
	     || (wp->cw.tgts == 0 && (wp->cw.honor & DCC_HONOR_SRVR_ISSPAM)))
	    && wp->cw.action != CMN_IGNORE)
		ascii_done(wp, DCCIF_RESULT_REJECT, "reject");

	if (wp->cw.honor & DCC_HONOR_GREY_EMBARGO) {
		totals.tgts_embargoed += wp->cw.tgts;
		ascii_done(wp, DCCIF_RESULT_GREY, "temporary greylist embargo");
	}

	if (wp->cw.deliver_tgts == wp->cw.tgts)
		ascii_done(wp, DCCIF_RESULT_OK, "accept");
	ascii_done(wp, DCCIF_RESULT_SOME, "accept for some");
}



#define SMTP_REPLY_221		"221 dccifd closing connection"
#define SMTP_REPLY_220		"220 dccifd proxy ready"
#define SMTP_REPLY_250_DATA	"250 dccifd mail ok"
#define SMTP_REPLY_250_RSET	"250 dccifd RSET ok"
#define SMTP_REPLY_250_HELO	"250 dccifd HELO ok"
#define SMTP_REPLY_250_POSTFIX	"250 dccifd Postfix extension ok"
#define SMTP_REPLY_250_RCPT	"250 dccifd Recipient ok"
#define SMTP_REPLY_250_MAIL	"250 dccifd Sender ok"
#define SMTP_REPLY_350		"354 Enter mail to dccifd"
#define SMTP_REPLY_500		"500 dccifd command unrecognized"
#define SMTP_REPLY_501_NO_ARG	"501 dccifd command arg required"
#define SMTP_REPLY_501_BAD_ARG	"501 dccifd command unrecognized"
#define SMTP_REPLY_501_RCPT	"501 dccifd RCPT command syntax error"
#define SMTP_REPLY_501_POSTFIX	"501 dccifd Postfix extension syntax error"
#define SMTP_REPLY_503_DATA	"503 dccifd need RCTP"
#define SMTP_REPLY_503		"503 Bad dccifd command sequence"

/* send an SMTP reply upstream to the SMTP client of our proxy */
static void
smtp_send_reply(WORK *wp, const char *reply, int len)
{
	buf_write(wp, &wp->reply_out, reply, len);
	buf_write(wp, &wp->reply_out, "\r\n", 2);
}

#define SMTP_REPLY(r) smtp_send_reply(wp,SMTP_REPLY_##r,STRZ(SMTP_REPLY_##r))

static void
smtp_reply_error(WORK *wp, const char *reply, int len)
{
	smtp_send_reply(wp, reply, len);
	wp->msg_rd.out = wp->msg_rd.next_line;
	wp->smtp_state = SMTP_ST_ERROR;
	if (dcc_clnt_debug)
		wp->cw.honor |= DCC_HONOR_LOGIT;
	log_print(&wp->cw, reply);
}

#define SMTP_ERROR(r) smtp_reply_error(wp,SMTP_REPLY_##r,STRZ(SMTP_REPLY_##r))



/* look for an SMTP verb */
typedef enum {
    SMTP_VERB_ERR,			/* bad command */
    SMTP_VERB_UNREC,			/* unrecognized command */
    SMTP_VERB_RSET,
    SMTP_VERB_HELO,
    SMTP_VERB_XCLIENT,			/* Postfix extension */
    SMTP_VERB_XFORWARD,			/* Postfix extension */
    SMTP_VERB_MAIL,			/* Mail From */
    SMTP_VERB_RCPT,			/* Rcpt To */
    SMTP_VERB_DATA,
    SMTP_VERB_QUIT,
} VERB_SMTP;
typedef struct {
    const char	*str;
    int		str_len;
    const char	*parm;
    int		parm_len;
    VERB_SMTP	verb;
    u_char	arg_required;
} PT;
PT pt[] = {
#define PT_M(s,p,v,r) {s,STRZ(s),p,STRZ(p),SMTP_VERB_##v,r}
    PT_M("HELO","", HELO, 1),
    PT_M("EHLO","", HELO, 1),
    PT_M("Mail","From:", MAIL, 1),
    PT_M("Rcpt","To:", RCPT, 1),
    PT_M("DATA","", DATA, 0),
    PT_M("XFORWARD","", XFORWARD, 1),
    PT_M("XCLIENT","", XCLIENT, 1),
    PT_M("RSET","", RSET, 0),
    PT_M("QUIT","", QUIT, 0),
#undef PT_M
};

#define RESP_DIGIT(c) ((c) >= '0' && (c) <= '9')

static VERB_SMTP
get_smtp_verb(WORK *wp,
	      const char **ppp,		/* parameter after command */
	      const char **pep)		/* end of parameter */
{
	PT *ptp;
	const char *lp, *pp;
	int i, len;

	/* skip leading whitespace */
	wp->msg_rd.out += strspn(wp->msg_rd.out, " \t");
	lp = wp->msg_rd.out;

	for (ptp = pt; ptp < &pt[DIM(pt)]; ++ptp) {
		len = ptp->str_len;
		pp = lp+len;
		if (pp+2 > wp->msg_rd.next_line
		    || strncasecmp(lp, ptp->str, len))
			continue;

		if (pp+2 == wp->msg_rd.next_line) {
			/* SMTP command by itself on the line */
			*ppp = 0;
			*pep = 0;
			if (ptp->arg_required) {
				SMTP_REPLY(501_NO_ARG);
				return SMTP_VERB_ERR;
			}
			return ptp->verb;
		}

		if (ptp->parm_len) {
			/* require parameter */
			i = strspn(pp, " \t");
			if (i == 0)
				continue;
			pp += i;
			if (*pp == '\r') {
				SMTP_REPLY(501_NO_ARG);
				return SMTP_VERB_ERR;
			}
			if (strncasecmp(pp, ptp->parm, ptp->parm_len)) {
				SMTP_REPLY(501_BAD_ARG);
				return SMTP_VERB_ERR;
			}
			pp += ptp->parm_len;
		}

		i = strspn(pp, " \t");
		if (i == 0)
			continue;
		pp += i;

		*ppp = pp;
		*pep = strpbrk(pp, " \t\r");
		return ptp->verb;
	}

	*ppp = 0;
	return SMTP_VERB_UNREC;
}



static void
proxy_rset(WORK *wp, const char *log_msg)
{
	if (log_msg
	    && wp->smtp_state != SMTP_ST_RSET
	    && wp->smtp_state != SMTP_ST_ERROR) {
		if (dcc_clnt_debug)
			wp->cw.honor |= DCC_HONOR_LOGIT;
		log_print(&wp->cw, log_msg);
	}

	cmn_clear(&wp->cw, wp);
	wp->flags &= FG_RECYCLE;

	wp->smtp_state = SMTP_ST_RSET;

	/* Values from the Postfix XCLIENT ESMTP extension endure for the
	 * entire session.  Values from the XFORWARD extension or guess from
	 * a HELO command are cleared at the end of the SMTP transaction. */
	if (!(wp->flags & FG_XCLIENT_NAME))
		wp->cw.clnt_name[0] = '\0';

	if (!(wp->flags & FG_XCLIENT_ADDR)) {
		if (wp->proxy_in_su.sa.sa_family == AF_UNIX) {
			wp->cw.clnt_str[0] = '\0';
			wp->cw.rcpt_st_flags |= RCPT_ST_NO_CLNT_ADDR;
		} else {
			/* assume IP address is the SMTP client to our proxy */
			if (wp->proxy_in_su.sa.sa_family == AF_INET) {
				dcc_ipv4toipv6(&wp->cw.clnt_addr,
					       wp->proxy_in_su.ipv4.sin_addr);
			} else if (wp->proxy_in_su.sa.sa_family == AF_INET6) {
				wp->cw.clnt_addr = (wp->proxy_in_su.ipv6
						    .sin6_addr);
			}
			if (!DCC_INET_NTOP(AF_INET6, &wp->cw.clnt_addr,
					   wp->cw.clnt_str,
					   sizeof(wp->cw.clnt_str))) {
				cmn_error_msg(&wp->cw,
					      "inet_ntop(addr of %s) failed",
					      wp->msg_rd.out);
				wp->cw.rcpt_st_flags |= RCPT_ST_NO_CLNT_ADDR;
			} else {
				dcc_get_ipv6_ck(&wp->cw.cks, &wp->cw.clnt_addr);
			}
		}
	}

	if (!(wp->flags & FG_XCLIENT_HELO))
		wp->cw.helo[0] = '\0';
}



static void
smtp_pass_line(WORK *wp)
{
	buf_write(wp, &wp->reply_out, wp->reply_in.out,
		  wp->reply_in.next_line - wp->reply_in.out);
	wp->reply_in.out = wp->reply_in.next_line;
}



/* pass an SMTP command downstream from the SMTP client of our proxy
 *	to the SMTP server of our proxy and then pass the response
 *	to the SMTP client */
static u_char				/* 0=server was unhappy */
smtp_pass(WORK *wp, const char *reply, int reply_len)
{
	int len;
	char dig, cont;

	/* fake it if the SMTP server is /dev/null */
	if (proxy_out_family == AF_UNSPEC) {
		smtp_send_reply(wp, reply, reply_len);
		return 1;
	}

	len = wp->msg_rd.next_line - wp->msg_rd.out;
	if (len != 0) {
		buf_write(wp, &wp->msg_wt, wp->msg_rd.out, len);
		buf_write_flush(wp, &wp->msg_wt);
	}

	/* wait for & pass on response */
	do {
		if (!msg_read_line(wp, &wp->reply_in))
			job_exit(wp);
		len = wp->reply_in.in - wp->reply_in.out;
		if (len < 5) {
			cmn_error_msg(&wp->cw, "short SMTP response"
				      " \"%s\" from proxy server",
				      wp->reply_in.out);
			job_exit(wp);
		}
		dig = wp->reply_in.out[0];
		cont = wp->reply_in.out[3];
		if ((dig < '1' || dig > '5')
		    || !RESP_DIGIT(wp->reply_in.out[1])
		    || !RESP_DIGIT(wp->reply_in.out[2])
		    || (cont != ' ' && cont != '-')) {
			*wp->reply_in.next_line = '\0';
			cmn_error_msg(&wp->cw, "unrecognized SMTP response"
				      " \"%s\" from proxy",
				      wp->reply_in.out);
			job_exit(wp);
		}

		smtp_pass_line(wp);
	} while (cont == '-');

	return (dig == reply[0]);
}

#define SMTP_PASS(r) smtp_pass(wp,SMTP_REPLY_##r,STRZ(SMTP_REPLY_##r))



/* parse Postfix XFORWRAD and XCLIENT commands
 * see http://www.postfix.org/XCLIENT_README.html
 * and http://www.postfix.org/XFORWARD_README.html */
static void
smtp_xpostfix(WORK *wp,
	      VERB_SMTP verb,		/* SMTP_VERB_XFORWARD or _XCLIENT */
	      const char *parm)
{
	const char *val, *next_parm;
	char addr[INET6_ADDRSTRLEN+1];
	int i;

	for (; *parm != '\r' && *parm != '\n'; parm = next_parm) {

		/* find the value */
		val = strpbrk(parm, "= \t\r\n");
		if (*val++ != '=') {
			SMTP_REPLY(501_POSTFIX);
			wp->msg_rd.out = wp->msg_rd.next_line;
			return;
		}

		next_parm = strpbrk(val, " \t\r\n");
		next_parm += strspn(next_parm, " \t");

		if (!CSTRCMP(parm, "NAME=")) {
			if (!CSTRCMP(val, "[UNAVAILABLE]")
			    || !CSTRCMP(val, "[TEMPUNAVAIL]")) {
				wp->cw.clnt_name[0] = '\0';
			} else {
				i = min(ISZ(wp->cw.clnt_name)-1, next_parm-val);
				memcpy(wp->cw.clnt_name, val, i);
				wp->cw.clnt_name[i] = '\0';
			}
			if (verb == SMTP_VERB_XCLIENT)
				wp->flags |= FG_XCLIENT_NAME;
			else
				wp->flags &= ~FG_XCLIENT_NAME;
			continue;
		}

		if (!CSTRCMP(parm, "ADDR=")) {
			if (!CSTRCMP(val, "[UNAVAILABLE]")
			    || !CSTRCMP(val, "[TEMPUNAVAIL]")) {
				wp->cw.rcpt_st_flags |= RCPT_ST_NO_CLNT_ADDR;
				if (verb == SMTP_VERB_XCLIENT)
					wp->flags |= FG_XCLIENT_ADDR;
				continue;
			}
			if (!CSTRCMP(val, "IPV6"))
				val += STRZ("IPV6");
			i = min(ISZ(addr)-1, next_parm-val);
			memcpy(addr, val, i);
			addr[i] = '\0';
			/* convert ASCCI representation of IP address to a
			 * canonical form and to a checksum */
			if (!dcc_get_str_ip_ck(&wp->cw.cks, addr,
					       &wp->cw.clnt_addr)) {
				cmn_error_msg(&wp->cw,
					      "unrecognized IP address \"%s\"",
					      addr);
				wp->cw.rcpt_st_flags |= RCPT_ST_NO_CLNT_ADDR;
				wp->flags &= ~FG_XCLIENT_ADDR;
				continue;
			}
			/* convert IPv6 address to a canonical string */
			if (!DCC_INET_NTOP(AF_INET6, &wp->cw.clnt_addr,
					   wp->cw.clnt_str,
					   sizeof(wp->cw.clnt_str))) {
				cmn_error_msg(&wp->cw,
					      "inet_ntop(addr of %s) failed",
					      addr);
				wp->cw.rcpt_st_flags |= RCPT_ST_NO_CLNT_ADDR;
				wp->flags &= ~FG_XCLIENT_ADDR;
				continue;
			}
			if (verb == SMTP_VERB_XCLIENT)
				wp->flags |= FG_XCLIENT_ADDR;
			else
				wp->flags &= ~FG_XCLIENT_ADDR;
			continue;
		}

		if (!CSTRCMP(parm, "HELO=")) {
			if (!CSTRCMP(val, "[UNAVAILABLE]")
			    || !CSTRCMP(val, "[TEMPUNAVAIL]")) {
				wp->cw.helo[0] = '\0';
			} else {
				get_helo(wp, val, next_parm-val);
			}
			if (verb == SMTP_VERB_XCLIENT)
				wp->flags |= FG_XCLIENT_HELO;
			else
				wp->flags &= ~FG_XCLIENT_HELO;
			continue;
		}
	}

	SMTP_PASS(250_POSTFIX);
	wp->msg_rd.out = wp->msg_rd.next_line;
}



/* parse SMTP Rcpt_To command */
static u_char
get_smtp_rcpt(WORK *wp,
	      const char *path,
	      const char *epath)
{
	const char *user;
	int path_len, user_len;

	path_len = epath - path;
	user = "";
	user_len = 0;
	if (!check_addr(wp, &path, &path_len, &user, &user_len)) {
		SMTP_REPLY(501_RCPT);
		wp->msg_rd.out = wp->msg_rd.next_line;
		return 0;
	}
	if (!SMTP_PASS(250_RCPT)) {
		wp->msg_rd.out = wp->msg_rd.next_line;
		return 0;
	}

	lock_work();
	wp->flags |= FG_WORK_LOCK;
	set_rcpt(wp, path, path_len, user, user_len);
	unlock_work();
	wp->flags &= ~FG_WORK_LOCK;

	wp->msg_rd.out = wp->msg_rd.next_line;
	return 1;
}



static void
smtp_data_done(WORK *wp, REPLY *reply, const char *result)
{
	char class;
	int len;

	if (reply) {
		/* do not pass the DATA command or the mail message to
		 * the SMTP server if we are rejecting the message */
		make_reply(&wp->cw, reply);
		smtp_send_reply(wp, wp->cw.reply_ptr, strlen(wp->cw.reply_ptr));

	} else if (proxy_out_family == AF_UNSPEC) {
		SMTP_REPLY(250_DATA);

	} else {
		/* send the message to the SMTP server for our proxy */
		buf_write(wp, &wp->msg_wt, "DATA\r\n", STRZ("DATA\r\n"));
		buf_write_flush(wp, &wp->msg_wt);

		/* the server should respond with a 350 result */
		if (!msg_read_line(wp, &wp->reply_in))
			job_exit(wp);
		len = wp->reply_in.in - wp->reply_in.out;
		if (len < 5) {
			cmn_error_msg(&wp->cw, "short SMTP response"
				      " \"%s\" from proxy server",
				      wp->reply_in.out);
			job_exit(wp);
		}
		class = wp->reply_in.out[0];
		if ((class < '3' || class > '5')
		    || !RESP_DIGIT(wp->reply_in.out[1])
		    || !RESP_DIGIT(wp->reply_in.out[2])
		    || wp->reply_in.out[3] != ' ') {
			*wp->reply_in.next_line = '\0';
			cmn_error_msg(&wp->cw,
				      "unrecognized SMTP response to DATA"
				      " \"%s\" from proxy",
				      wp->reply_in.out);
			job_exit(wp);
		}
		if (class == '3') {
			wp->reply_in.out = wp->reply_in.next_line;

			/* send the mail message to the SMTP server
			 * and listen to what it has to say */
			body_copy(wp);
			buf_write(wp, &wp->msg_wt, ".\r\n", 3);
			buf_write_flush(wp, &wp->msg_wt);
			if (!msg_read_line(wp, &wp->reply_in))
				job_exit(wp);
		}

		if (wp->reply_in.out[0] != '2') {
			len = min(ISZ(wp->cw.reply_buf)-1,
				  wp->reply_in.next_line - wp->reply_in.out);
			memcpy(wp->cw.reply_buf, wp->reply_in.out, len);
			wp->cw.reply_buf[len] = '\0';
			result = wp->cw.reply_buf;
		}
		/* pass SMTP server's response to the SMTP client */
		smtp_pass_line(wp);
	}

	LOG_CAPTION(wp, "result: ");
	log_write(&wp->cw, result, 0);
	LOG_EOL(wp);

	proxy_rset(wp, 0);
}



static void
smtp_data(WORK *wp)
{
	if (!wp->cw.num_rcpts) {
		SMTP_ERROR(503_DATA);
		return;
	}

	/* tell STMP client to send the mail message */
	wp->msg_rd.out = wp->msg_rd.next_line;
	SMTP_REPLY(350);
	buf_write_flush(wp, &wp->reply_out);

	if (!get_body(wp)) {
		smtp_data_done(wp, &temp_reply, "temporary rejection");
		return;
	}

	if ((wp->cw.reject_tgts != 0
	     || (wp->cw.tgts == 0 &&
			(wp->cw.honor & DCC_HONOR_SRVR_ISSPAM)))
		   && wp->cw.action != CMN_IGNORE) {
		smtp_data_done(wp, &rej_reply, "reject");
		return;
	}

	if (wp->cw.honor & DCC_HONOR_GREY_EMBARGO) {
		totals.tgts_embargoed += wp->cw.tgts;
		smtp_data_done(wp, &grey_reply, "temporary greylist embargo");
		return;
	}

	if (wp->cw.deliver_tgts == wp->cw.tgts) {
		smtp_data_done(wp, 0, "accept");
		return;
	} else {
		/* it would be nice if we could reject for only some */
		smtp_data_done(wp, &rej_reply, "reject");
		return;
	}
}



static void NRATTRIB
proxy_run(WORK *wp)
{
	VERB_SMTP verb;
	const char *parmp, *eparmp;
	int i;

	wp->smtp_state = SMTP_ST_RSET;
	SMTP_PASS(220);

	for (;;) {
		buf_write_flush(wp, &wp->reply_out);
		if (!msg_read_line(wp, &wp->msg_rd)) {
			proxy_rset(wp, "disconnected\n");
			job_exit(wp);
		}
		verb = get_smtp_verb(wp, &parmp, &eparmp);

		/* deal with common SMTP commands */
		switch (verb) {
		case SMTP_VERB_ERR:
			wp->msg_rd.out = wp->msg_rd.next_line;
			continue;
		case SMTP_VERB_UNREC:
			/* pass anything we don't recognize to the SMTP server
			 * for our proxy.
			 * If we don't have a server, reject it */
			SMTP_PASS(500);
			wp->msg_rd.out = wp->msg_rd.next_line;
			continue;
		case SMTP_VERB_QUIT:
			SMTP_PASS(221);
			proxy_rset(wp, "SMTP QUIT\n");
			job_exit(wp);
		case SMTP_VERB_RSET:
			SMTP_PASS(250_RSET);
			proxy_rset(wp, "SMTP RSET\n");
			wp->msg_rd.out = wp->msg_rd.next_line;
			continue;
		case SMTP_VERB_HELO:
			proxy_rset(wp, 0);
			/* save helo value in case there is no XPOSTFIX */
			if (SMTP_PASS(250_HELO)
			    && wp->cw.helo[0] == '\0'
			    && !(wp->flags & FG_XCLIENT_HELO))
				get_helo(wp, parmp, eparmp - parmp);
			wp->msg_rd.out = wp->msg_rd.next_line;
			continue;
		default:
			break;
		}

		switch (wp->smtp_state) {
		case SMTP_ST_RSET:	/* expecting HELO or perhaps MAIL */
			switch (verb) {
			case SMTP_VERB_XCLIENT:
			case SMTP_VERB_XFORWARD:
				smtp_xpostfix(wp, verb, parmp);
				continue;
			case SMTP_VERB_MAIL:
				/* get sender */
				if (SMTP_PASS(250_MAIL)) {
					i = eparmp - parmp;
					if (i > ISZ(wp->env_from)-1)
					    i = ISZ(wp->env_from)-1;
					memcpy(wp->env_from, parmp, i);
					wp->env_from[i] = '\0';
					wp->smtp_state = SMTP_ST_TRANS;
					log_start(&wp->cw);
				}
				wp->msg_rd.out = wp->msg_rd.next_line;
				continue;
			default:
				break;
			}
			break;

		case SMTP_ST_TRANS:	/* seen Mail_From */
			if (verb ==  SMTP_VERB_RCPT) {
				if (get_smtp_rcpt(wp, parmp, eparmp))
					wp->smtp_state = SMTP_ST_RCPT;
				continue;
			}
			break;

		case SMTP_ST_RCPT:	/* seen Rctp_To */
			switch (verb) {
			case SMTP_VERB_RCPT:
				get_smtp_rcpt(wp, parmp, eparmp);
				continue;
			case SMTP_VERB_DATA:
				smtp_data(wp);
				continue;
			default:
				break;
			}
			break;

		case SMTP_ST_ERROR:	/* no transaction until RSET or HELO */
			break;
		}

		/* reject anything out of sequence */
		SMTP_ERROR(503);
	}
}



/* use a subset of SMTP to talk to an MTA */
static void NRATTRIB
proxy_job(WORK *wp)
{
	DCC_SOCKU su;

	log_start(&wp->cw);

	if (proxy_out_family == AF_UNSPEC) {
		/* single-ended */
		;

	} else if (proxy_out_family == AF_UNIX) {
		wp->proxy_out_soc = socket(AF_UNIX, SOCK_STREAM, 0);
		if (wp->proxy_out_soc < 0) {
			cmn_error_msg(&wp->cw, "proxy_out socket(AF_UNIX): %s",
				      ERROR_STR());
			job_exit(wp);
		}
		if (0 > connect(wp->proxy_out_soc,
				(struct sockaddr *)&proxy_out_sun,
				sizeof(listen_sun))) {
			cmn_error_msg(&wp->cw,
				      "proxy_out connect(AF_UNIX,%s): %s",
				      proxy_out_sun.sun_path,
				      ERROR_STR());
			job_exit(wp);
		}
		if (!set_soc(wp->cw.emsg, wp->proxy_out_soc, AF_UNIX,
			     "proxy output")) {
			cmn_error_msg(&wp->cw, "%s", wp->cw.emsg);
			job_exit(wp);
		}
		wp->flags |= FG_MTA_BODY;

	} else {
		if (proxy_out_su.sa.sa_family == AF_UNSPEC) {
			/* use SMTP client's IP address */
			if (wp->proxy_in_su.sa.sa_family == AF_INET)
				dcc_mk_su(&su, AF_INET,
					  &wp->proxy_in_su.ipv4.sin_addr,
					  proxy_out_port);
			else
				dcc_mk_su(&su, AF_INET6,
					  &wp->proxy_in_su.ipv6.sin6_addr,
					  proxy_out_port);
		} else {
			su = proxy_out_su;
		}
		wp->proxy_out_soc = socket(su.sa.sa_family, SOCK_STREAM, 0);
		if (wp->proxy_out_soc < 0) {
			cmn_error_msg(&wp->cw, "proxy_out socket(%s): %s",
				      dcc_su2str(&su), ERROR_STR());
			job_exit(wp);
		}
		if (0 > connect(wp->proxy_out_soc, &su.sa, DCC_SU_LEN(&su))) {
			cmn_error_msg(&wp->cw,
				      "proxy_out connect(%s): %s",
				      dcc_su2str(&su), ERROR_STR());
			job_exit(wp);
		}
		if (!set_soc(wp->cw.emsg, wp->proxy_out_soc, su.sa.sa_family,
			     "proxy output")) {
			cmn_error_msg(&wp->cw, "%s", wp->cw.emsg);
			job_exit(wp);
		}
		wp->flags |= FG_MTA_BODY;
	}

	wp->msg_rd.base = wp->buf1;
	wp->msg_rd.size = sizeof(wp->buf1);
	wp->msg_rd.socp = &wp->proxy_in_soc;

	wp->msg_wt.base = wp->buf2;
	wp->msg_wt.size = sizeof(wp->buf2);
	wp->msg_wt.socp = &wp->proxy_out_soc;

	wp->reply_in.base = wp->buf3;
	wp->reply_in.size = sizeof(wp->buf3);
	wp->reply_in.socp = &wp->proxy_out_soc;

	wp->reply_out.base = wp->buf4;
	wp->reply_out.size = sizeof(wp->buf4);
	wp->reply_out.socp = &wp->proxy_in_soc;

	proxy_run(wp);
}



/* start a new connection to an MTA */
static void *
job_start(void *wp0)
{
	WORK *wp = wp0;
	sigset_t sigsoff;
	int i;

	/* working threads do not deal with signals */
	sigemptyset(&sigsoff);
	sigaddset(&sigsoff, SIGHUP);
	sigaddset(&sigsoff, SIGINT);
	sigaddset(&sigsoff, SIGTERM);
	i = pthread_sigmask(SIG_BLOCK, &sigsoff, 0);
	if (i)
		dcc_logbad(EX_SOFTWARE, "pthread_sigmask(): %s",
			   ERROR_STR1(i));

	log_start(&wp->cw);

	if (proxy)
		proxy_job(wp);
	else
		ascii_job(wp);
}
