Mercurial > notdcc
diff dccifd/dccifd.c @ 0:c7f6b056b673
First import of vendor version
author | Peter Gervai <grin@grin.hu> |
---|---|
date | Tue, 10 Mar 2009 13:49:58 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dccifd/dccifd.c Tue Mar 10 13:49:58 2009 +0100 @@ -0,0 +1,3357 @@ +/* Distributed Checksum Clearinghouse + * + * DCC interface daemon + * + * Copyright (c) 2008 by Rhyolite Software, LLC + * + * This agreement is not applicable to any entity which sells anti-spam + * solutions to others or provides an anti-spam solution as part of a + * security solution sold to other entities, or to a private network + * which employs the DCC or uses data provided by operation of the DCC + * but does not provide corresponding data to other users. + * + * Permission to use, copy, modify, and distribute this software without + * changes for any purpose with or without fee is hereby granted, provided + * that the above copyright notice and this permission notice appear in all + * copies and any distributed versions or copies are either unchanged + * or not called anything similar to "DCC" or "Distributed Checksum + * Clearinghouse". + * + * Parties not eligible to receive a license under this agreement can + * obtain a commercial license to use DCC by contacting Rhyolite Software + * at sales@rhyolite.com. + * + * A commercial license would be for Distributed Checksum and Reputation + * Clearinghouse software. That software includes additional features. This + * free license for Distributed ChecksumClearinghouse Software does not in any + * way grant permision to use Distributed Checksum and Reputation Clearinghouse + * software + * + * THE SOFTWARE IS PROVIDED "AS IS" AND RHYOLITE SOFTWARE, LLC DISCLAIMS ALL + * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL RHYOLITE SOFTWARE, LLC + * BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES + * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + * SOFTWARE. + * + * Rhyolite Software DCC 1.3.103-1.166 $Revision$ + */ + +#include "dccif.h" +#include "dcc_paths.h" +#include "cmn_defs.h" +#include <signal.h> + + +static int stopping; + +#define MAX_SMTP_LINE + +u_char use_ipv6 = 0; + +/* incoming proxy and bidirectional ASCII dccifd protocol connection */ +static char *listen_addr; +static u_char listen_family; +static struct sockaddr_un listen_sun; + +static const char *lhost_port; +static const char *rcidr; +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[DCC_MAXDOMAINLEN]; +static u_int16_t proxy_out_port; +static DCC_SOCKU proxy_out_su; + +static const char *rundir = DCC_RUNDIR; +static DCC_PATH pidpath; +static const char *progpath = DCC_LIBEXECDIR"/dccifd"; + +static u_char background = 1; + +static u_char proxy; + +u_char cannot_discard = 0; /* can trim targets after DATA */ +u_char cannot_reject = 1; /* ASCII protocol accepts all targets */ + + +/* 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 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_START, /* expecting HELO */ + SMTP_ST_HELO, /* seen HELO, 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; + /* 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; + int parse_rcvd; /* which received: header to parse */ + u_int dfgs; +# define DFG_WORK_LOCK 0x0001 /* hold the lock */ +# define DFG_MISSING_BODY 0x0002 /* missing message body */ +# define DFG_MTA_BODY 0x0004 /* MTA wants the body */ +# define DFG_MTA_HEADER 0x0008 /* MTA wants the X-DCC header */ +# define DFG_SEEN_HDR 0x0010 /* have at least 1 header */ +# define DFG_PARSE_RCVD 0x0020 /* parse Received header */ +# define DFG_XCLIENT_NAME 0x0040 /* client name via XCLIENT */ +# define DFG_XCLIENT_ADDR 0x0080 /* client addresses via XCLIENT */ +# define DFG_XCLIENT_HELO 0x0100 /* HELO value via XCLIENT */ +# define DFG_RECYCLE (DFG_WORK_LOCK | DFG_MTA_BODY \ + | DFG_XCLIENT_NAME | DFG_XCLIENT_ADDR \ + | DFG_XCLIENT_HELO) +} WORK; + +/* use a free list to avoid malloc() overhead */ +static WORK *work_free; + +/* each dccifd 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. + * The file descriptors for the whitelists are accounted for in EXTRA_FILES */ +#define FILES_PER_JOB 4 +int max_max_work = MAX_SELECT_WORK; + + +typedef struct user_domain { + struct user_domain *fwd; + const char *nm; + int len; + u_char wildcard; +} USER_DOMAIN; +static USER_DOMAIN *user_domains; +static char hostname[MAXHOSTNAMELEN+1] = "@"; + +/* 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_listen(void); +static void close_listen_soc(void); +static void unlink_listen_sun(void); +static void NRATTRIB *job_start(void *); +static void job_close(WORK *); +static void NRATTRIB job_exit(WORK *); +static void NRATTRIB proxy_msg_truncated(WORK *); +static void add_work(int); + + +static void +usage(const char* barg, const char *bvar) +{ + const char str[] = { + "usage: [-VdbxANQ] [-G on | off | noIP | IPmask/xx] [-h homedir]" + " [-I user]\n" + " [-p /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 | DISCARD]\n" + " [-t type,[log-thold,][rej-thold]] [-g [not-]type]" + " [-S header]\n" + " [-l logdir] [-R rundir] [-T tmpdir] [-j maxjobs]\n" + " [-B dnsbl-option] [-L ltype,facility.level]\n" + }; + static u_char complained; + + if (!complained) { + if (barg) + dcc_error_msg("unrecognized \"%s%s\"\nusage: %s\n..." + " continuing", + barg, bvar, str); + else + dcc_error_msg("%s\n... continuing", str); + complained = 1; + } +} + + +int NRATTRIB +main(int argc, char **argv) +{ + DCC_EMSG emsg; +#ifdef RLIMIT_NOFILE + struct rlimit nofile; + int old_rlim_cur; +#endif + long l; + u_char log_tgts_set = 0; + const char *homedir = 0; + const char *logdir = 0; + const char *tmpdir = 0; + WORK *wp; + pthread_t tid; + DCC_SOCKLEN_T 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_clear_tholds(); + +#ifdef RLIMIT_NOFILE + if (0 > getrlimit(RLIMIT_NOFILE, &nofile)) { + dcc_error_msg("getrlimit(RLIMIT_NOFILE): %s", ERROR_STR()); + old_rlim_cur = 1000*1000; + } else { + old_rlim_cur = nofile.rlim_cur; + if (nofile.rlim_max < 1000*1000) { + i = nofile.rlim_max; +#ifndef USE_POLL + if (i > FD_SETSIZE) + i = FD_SETSIZE; +#endif + max_max_work = (i - EXTRA_FILES)/FILES_PER_JOB; + max_max_work_src = "RLIMIT_NOFILE limit"; + } + } +#endif /* RLIMIT_NOFILE */ + if (max_max_work <= 0) { + dcc_error_msg("too few open files allowed"); + max_max_work = MIN_MAX_WORK; + } + max_work = max_max_work; + +#define SLARGS "64VdbxANQW" /* fix start-dccifd if these change */ + while (-1 != (i = getopt(argc, argv, SLARGS"G:h:I: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("-G", optarg); + break; + + case 'W': /* obsolete DCC off by default */ + to_white_only = 1; + break; + + case 'h': + homedir = optarg; + break; + + case 'I': + dcc_daemon_su(optarg); + break; + + case 'p': + listen_addr = optarg; + break; + + case 'o': + proxy = 1; + cannot_reject = 0; + cannot_discard = 1; + p = strchr(optarg, ','); + if (!p) { + /* recognize single-ended (not-really-)-proxy */ + if (!strcmp(optarg, _PATH_DEVNULL)) { + 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 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); + proxy_out_family = AF_INET; /* includes IPv6 */ + } + break; + + case 'D': + /* save user domain names sorted by length + * so that we can apply the most restrictive */ + i = strlen(optarg); + if (*optarg == '\0' + || ((optarg[0] == '@' || optarg[0] == '*') + && i < 2) + || strpbrk(optarg+1, "@*")) { + dcc_logbad(EX_USAGE, + "invalid local-domain \"%s\"", + optarg); + break; + } + udom2 = dcc_malloc(sizeof(*udom2)); + memset(udom2, 0, sizeof(*udom2)); + udom2->len = i; + udom2->nm = optarg; + if (optarg[0] == '*') { + udom2->wildcard = 1; + ++udom2->nm; + --udom2->len; + } + udomp = &user_domains; + for (;;) { + udom = *udomp; + /* insert longer names before shorter names + * insert wildcard before same, non-wildcard + * because i includes the '*' */ + 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, 10); + if (*p != '\0' || l < MIN_MAX_WORK) { + dcc_error_msg("invalid queue length %s", + optarg); + } else if (l > max_max_work) { + dcc_error_msg("-j queue length %s" + " larger than %s; using %d", + optarg, + max_max_work_src, max_max_work); + max_work = max_max_work; + } else { + max_work = l; + } + break; + + case 'B': + if (!dcc_parse_dnsbl(emsg, optarg, progpath, 0)) + dcc_error_msg("%s", emsg); + break; + + case 'L': + if (dcc_parse_log_opt(optarg)) + helper_save_arg("-L", optarg); + break; + + default: + usage(optopt2str(optopt), ""); + } + } + argc -= optind; + argv += optind; + if (argc != 0) + usage(argv[0], ""); + + /* default -D setting to the local host name */ + if (!user_domains) { + if (0 > gethostname(hostname+1, sizeof(hostname)-2)) { + dcc_error_msg("gethostname(): %s", ERROR_STR()); + } else if ((i = strlen(hostname)) > 1) { + user_domains = dcc_malloc(sizeof(*user_domains)); + memset(user_domains, 0, sizeof(*user_domains)); + user_domains->len = i; + user_domains->nm = hostname; + } + } + + dcc_cdhome(0, homedir, 0); + dcc_main_logdir_init(0, logdir); + tmp_path_init(tmpdir, logdir); + + if (proxy_out_family == AF_INET) { + 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_PORTP(&proxy_out_su) = proxy_out_port; + dcc_host_unlock(); + } + } + + /* Open the incoming socket before our backgrounding fork() to + * minimize races and allow better error reporting. */ + bind_listen(); + + if (dcc_main_logdir[0] == '\0') { + if (log_tgts_set) + dcc_error_msg("log thresholds set with -t" + " but no -l directory"); + if (userdirs != '\0') + dcc_error_msg("no -l directory prevents per-user" + " logging with -U"); + } + +#ifdef RLIMIT_NOFILE + 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" + " by RLIMIT_NOFILE", + old_rlim_cur); + max_work = MIN_MAX_WORK; + } + } + } +#endif /* RLIMIT_NOFILE */ + + helper_init(max_work); + + if (background) { + if (daemon(1, 0) < 0) + dcc_logbad(EX_OSERR, "daemon(): %s", ERROR_STR()); + + dcc_daemon_restart(rundir, 0); + dcc_pidfile(pidpath, rundir); + } + + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, sigterm); + signal(SIGTERM, sigterm); + signal(SIGINT, sigterm); +#ifdef SIGXFSZ + signal(SIGXFSZ, SIG_IGN); +#endif + + /* Be careful to start all threads only after the fork() in daemon(), + * because some POSIX threads packages (e.g. FreeBSD) get confused + * about threads in the parent. */ + + cmn_init(); + add_work(init_work); + + if (listen_family != AF_UNIX) { + dcc_trace_msg(DCC_VERSION" listening to %s from %s for %s", + lhost_port, rcidr, + proxy ? "SMTP commands" : "ASCII protocol"); + } else { + dcc_trace_msg(DCC_VERSION" listening to %s for %s", + listen_addr, + proxy ? "SMTP commands" : "ASCII protocol"); + } + if (dcc_clnt_debug) + dcc_trace_msg("init_work=%d max_work=%d max_max_work=%d (%s)", + total_work, max_work, max_max_work, + max_max_work_src); + + 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_US); + 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) { + if (total_work > max_work) { + /* pretend we weren't listening if we + * are out of context blocks */ + unlock_work(); + sleep(1); + continue; + } + if (dcc_clnt_debug > 1) + dcc_trace_msg("add %d work blocks to %d", + init_work, total_work); + add_work(init_work); + wp = work_free; + } + work_free = wp->fwd; + unlock_work(); + + /* clear most of context block for the new connection */ + cmn_clear(&wp->cw, wp, 1); + wp->cw.helo[0] = '\0'; + 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)) { + char str[DCC_SU2STR_SIZE]; + dcc_error_msg("unauthorized client address %s", + dcc_su2str(str, sizeof(str), + &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; + } + i = pthread_create(&tid, 0, job_start, wp); + if (i) { + dcc_error_msg("pthread_create(): %s", ERROR_STR1(i)); + job_close(wp); + continue; + } + 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)); + } + } + + close_listen_soc(); + + totals_stop(); + + exit(stopping); +} + + + +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; + int i; + +#ifdef HP_UX_BAD_AF_UNIX + dcc_logbad(EX_CONFIG, "HP-UX AF_UNIX does not support shutdown()"); +#endif + + 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 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. Do not give up + * immediately in case a previous instance is slowly stopping. */ + i = 0; + for (;;) { + 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()); + } + close(listen_soc); + break; + } + /* connect() worked so the socket is alive */ + if (++i > 5*10) + dcc_logbad(EX_UNAVAILABLE, + "something running with socket at %s", + listen_sun.sun_path); + close(listen_soc); + usleep(100*1000); + } + + 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 lhost[MAXHOSTNAMELEN]; + u_int16_t lport; + DCC_SOCKU su; + char *duparg; + const char *cp; + int on, error; + + emsg[0] = '\0'; + + duparg = strdup(listen_addr); + lhost_port = duparg; + duparg = strchr(duparg, ','); + if (!duparg) + dcc_logbad(EX_USAGE, "missing port number in \"%s\"", + listen_addr); + + duparg = strchr(duparg+1, ','); + if (!duparg) + dcc_logbad(EX_USAGE, "missing rhost in \"%s\"", + listen_addr); + *duparg++ = '\0'; + + rcidr = duparg; + if (0 >= dcc_str2cidr(emsg, &raddr, &rmask, 0, rcidr, 0, 0)) + dcc_logbad(EX_USAGE, "invalid rhost and mask \"%s\"", + rcidr); + + cp = dcc_parse_nm_port(emsg, lhost_port, DCC_GET_PORT_INVALID, + lhost, sizeof(lhost), &lport, 0, 0, 0, 0); + if (!cp) + dcc_logbad(dcc_ex_code, "%s", emsg); + if (*cp != '\0') + dcc_logbad(EX_USAGE, "invalid IP address: \"%s\"", + lhost_port); + + if (lhost[0] == '\0' || !strcmp(lhost, "@")) { + /* null or "@" means INADDR_ANY */ + dcc_mk_su(&su, use_ipv6 ? AF_INET6: AF_INET, 0, lport); + } else { + dcc_host_lock(); + if (!dcc_get_host(lhost, use_ipv6, &error)) + dcc_logbad(EX_NOHOST, "%s: %s", + lhost, DCC_HSTRERROR(error)); + su = dcc_hostaddrs[0]; + *DCC_SU_PORTP(&su) = lport; + 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_err(&su), ERROR_STR()); + + if (0 > bind(listen_soc, &su.sa, DCC_SU_LEN(&su))) + dcc_logbad(EX_UNAVAILABLE, "bind(%s) %s", + lhost_port, 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 void +bind_listen(void) +{ + DCC_EMSG emsg; + char *p; + + /* 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()); +} + + + +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) { + thr_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->dfgs & DFG_WORK_LOCK) + unlock_work(); + + if (!bc->socp || *bc->socp < 0) + dcc_logbad(EX_SOFTWARE, "attempt to read closed socket"); + + for (;;) { + i = dcc_select_poll(wp->cw.emsg, *bc->socp, 1, + MAX_MTA_DELAY*DCC_US); + if (i > 0) + break; + if (i < 0) { + thr_error_msg(&wp->cw, "%s", wp->cw.emsg); + } else { + thr_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) + thr_error_msg(&wp->cw, "read(sock): %s", ERROR_STR()); + job_exit(wp); + } + bc->in += total; + + if (wp->dfgs & DFG_WORK_LOCK) + lock_work(); + return total != 0; +} + + + +/* ensure there is another line in the buffer */ +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 || *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_US); + if (i < 0) { + thr_error_msg(&wp->cw, "%s", wp->cw.emsg); + job_exit(wp); + } + if (i == 0) { + thr_error_msg(&wp->cw, "MTA write timeout"); + job_exit(wp); + } + i = write(*bc->socp, buf, len); + if (i < 0) { + if (DCC_BLOCK_ERROR()) + continue; + thr_error_msg(&wp->cw, "write(MTA socket,%d): %s", + len, ERROR_STR()); + job_exit(wp); + } + if (i == 0) { + thr_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 == 0) + dcc_logbad(EX_SOFTWARE, "impossible buffer space"); + 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) +{ + if (!cmn_write_tmp(&wp->cw, buf, len)) { + thr_error_msg(&wp->cw, "%s", wp->cw.emsg); + job_exit(wp); + } +} + + + +static int +tmp_read(WORK *wp, void *buf, int len) +{ + int i; + + if (wp->cw.tmp_fd < 0) + return 0; + + i = read(wp->cw.tmp_fd, buf, len); + if (i < 0) { + thr_error_msg(&wp->cw, "read(%s,%d): %s", + wp->cw.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; +} + + + +/* Create the contexts. */ +static void +add_work(int i) +{ + WORK *wp; + + total_work += i; + + wp = dcc_malloc(sizeof(*wp)*i); + memset(wp, 0, sizeof(*wp)*i); + + while (i-- != 0) { + wp->proxy_in_soc = -1; + wp->proxy_out_soc = -1; + cmn_create(&wp->cw); + wp->fwd = work_free; + work_free = wp; + ++wp; + } +} + + + +void +work_clean(void) +{ + WORK *wp; + int keep, delete; + + lock_work(); + keep = 5; + delete = init_work; + for (wp = work_free; wp; wp = wp->fwd) { + if (!wp->cw.dcc_ctxt) + break; + if (--keep > 0) + continue; + dcc_clnt_soc_close(wp->cw.dcc_ctxt); + if (--delete <= 0) + break; + } + unlock_work(); +} + + + +static void +job_close(WORK *wp) +{ + if (wp->dfgs & DFG_WORK_LOCK) { + wp->dfgs &= ~DFG_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; + } + + cmn_close_tmp(&wp->cw); + + if (wp->proxy_in_soc >= 0) { + if (0 > close(wp->proxy_in_soc) + && (dcc_clnt_debug + || (errno != ECONNRESET + && errno != ENOTCONN))) + thr_error_msg(&wp->cw, "close(proxy input socket): %s", + ERROR_STR()); + wp->proxy_in_soc = -1; + } + if (wp->proxy_out_soc >= 0) { + if (0 > close(wp->proxy_out_soc)) + thr_error_msg(&wp->cw, "close(proxy output socket): %s", + ERROR_STR()); + wp->proxy_out_soc = -1; + } + log_stop(&wp->cw); + + lock_work(); + free_rcpt_sts(&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_OSERR, "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 <= 0) + 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; + const char *nl; + char *bol, *eol; + int i; + + if (-1 == lseek(wp->cw.tmp_fd, 0, SEEK_SET)) { + thr_error_msg(&wp->cw, "rewind %s: %s", + wp->cw.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 + && wp->msg_rd.out != wp->msg_rd.base) + break; + /* pretend the header ended with the buffer + * if there is no room */ + eol = wp->msg_rd.in-1; + nl = 0; + } else if (eol+1 >= wp->msg_rd.in) { + /* get the character after '\n' */ + if (i < wp->msg_rd.size + && wp->msg_rd.out != wp->msg_rd.base) + break; + /* pretend we could not find it if there + * is no room in the buffer + * or if we are at end of file. + * This will also end the headers */ + 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 field name. */ + if (chghdr == SETHDR + && is_xhdr(bol, eol-bol)) { + /* skip it + * and 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 SMTP transparency */ + if (proxy && *nl == '.') + *eol-- = '.'; + } + + bol = eol+1; + } + + mta_write(wp, bol); + } +} + + + +static void +add_hdr(void *wp0, const char *buf, u_int buf_len) +{ + WORK *wp = wp0; + + buf_write(wp, &wp->msg_wt, buf, buf_len); +} + + + +static void +body_copy(WORK *wp) +{ + u_char seen_crlf; + char *p; + + hdrs_copy(wp); + + if (chghdr != NOHDR && wp->cw.header.buf[0] != '\0') { + /* write X-DCC header + * end with "\r\n" if at least + * half of the header lines ended that way */ + xhdr_write(add_hdr, wp, wp->cw.header.buf, + wp->cw.header.used, + wp->cr_hdrs > wp->total_hdrs/2); + } + + /* 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 (pidpath[0] != '\0') { + unlink(pidpath); + pidpath[0] = '\0'; + } + + if (listen_soc >= 0) { + 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 = 100 + sig; + unlink_listen_sun(); + dcc_clnt_stop_resolve(); + signal(sig, SIG_DFL); /* quit on repeated signals */ +} + + + +void +user_reject_discard(UATTRIB CMN_WORK *cwp, UATTRIB RCPT_ST *rcpt_st) +{ + /* dccifd has no way to tell the MTA to remove a recipient + * after the Rcpt_To command */ + thr_error_msg(cwp, "cannot discard an individual target"); +} + + + +static void +get_helo(WORK *wp, const char *vp, int len) +{ + if (len < DCC_HELO_MAX-1) { + memcpy(wp->cw.helo, vp, len); + wp->cw.helo[len] = '\0'; + } else { + /* if the HELO value is too long, capture what we can + * and indicate that we got only part of it */ + len = DCC_HELO_MAX-1; + memcpy(wp->cw.helo, vp, len); + 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. + * That is impossible for the ASCII dccifd + * protocol because there should be at least + * a Received header. + * When running as a proxy, we should get + * "\r\n.\r\n" + * Fake newlines until we get out of here + * so that we will log whatever we got */ + wp->dfgs |= DFG_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) { + /* ".\r\n" at the start of a line + * ends the message. In this case + * the message body including the + * separating blank line is missing. + * Stop short before the ".\r\n" */ + 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; + /* quit at the end of headers */ + if (hdr_len == 1 + || (hdr_len == 2 && *wp->msg_rd.out == '\r')) + return 0; + at_bol = 1; + } else { + hdr_len += nlen; + at_bol = 0; + } + } +} + + + +static void +get_hdrs(WORK *wp) +{ + const char *p, *rh; + + 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 (!CLITCMP(wp->msg_rd.out, h)) { \ + dcc_get_cks(&wp->cw.cks, DCC_CK_##t, \ + &wp->msg_rd.out[LITZ(h)], 1); \ + wp->dfgs |= DFG_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 + + /* notice UNIX From_ line */ + if (!(wp->dfgs & DFG_SEEN_HDR) + && wp->cw.env_from[0] == '\0' + && parse_unix_from(wp->msg_rd.out, wp->cw.env_from, + sizeof(wp->cw.env_from))) { + wp->dfgs |= DFG_SEEN_HDR; + wp->msg_rd.out = wp->msg_rd.next_line; + continue; + } + + if (wp->cw.env_from[0] == '\0' + && parse_return_path(wp->msg_rd.out, wp->cw.env_from, + sizeof(wp->cw.env_from))) { + wp->dfgs |= DFG_SEEN_HDR; + wp->msg_rd.out = wp->msg_rd.next_line; + continue; + } + + if (!CLITCMP(wp->msg_rd.out, DCC_XHDR_TYPE_RECEIVED":")) { + p = &wp->msg_rd.out[LITZ(DCC_XHDR_TYPE_RECEIVED":")]; + + /* compute checksum of the last Received: header */ + dcc_get_cks(&wp->cw.cks, DCC_CK_RECEIVED, p, 1); + + wp->dfgs |= DFG_SEEN_HDR; + wp->msg_rd.out = wp->msg_rd.next_line; + + /* pick IP address out of first Received: header */ + if (!(wp->dfgs & DFG_PARSE_RCVD) + || --wp->parse_rcvd >= 0) + continue; + rh = parse_received(p, &wp->cw.cks, + (wp->cw.helo[0] == '\0') + ? wp->cw.helo : 0, + sizeof(wp->cw.helo), + wp->cw.sender_str, + sizeof(wp->cw.sender_str), + wp->cw.sender_name, + sizeof(wp->cw.sender_name)); + if (rh == 0) { + /* to avoid being fooled by forged + * headers, stop at a strange one */ + wp->dfgs &= ~DFG_PARSE_RCVD; + + } else if (*rh != '\0') { + thr_log_print(&wp->cw, 1, + "skip %s Received: header\n", rh); + + } else if (!check_mx_listing(&wp->cw)) { + /* we know the client */ + wp->dfgs &= ~DFG_PARSE_RCVD; + } + 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->dfgs |= DFG_SEEN_HDR; + + /* notice any sort of header */ + if (!(wp->dfgs & DFG_SEEN_HDR)) { + for (p = wp->msg_rd.out; ; ++p) { + if (*p == ':') { + wp->dfgs |= DFG_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); +} + + + +/* Assume for now that the sender is the SMTP client. Received: headers + * might challenge that assumption */ +static void +get_sender(WORK *wp) +{ + strcpy(wp->cw.sender_name, wp->cw.clnt_name); + strcpy(wp->cw.sender_str, wp->cw.clnt_str); + if (wp->cw.sender_str[0] == '\0' + || check_mx_listing(&wp->cw)) { + /* try Received: header if client is unknown or MX server*/ + wp->dfgs |= DFG_PARSE_RCVD; + } +} + + + +static u_char /* 0=temporary failure, 1=ok */ +get_body(WORK *wp) +{ + char *p; + 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. */ + if (!cmn_open_tmp(&wp->cw)) { + if (wp->dfgs & DFG_MTA_BODY) { + dcc_error_msg("fatal error: %s", wp->cw.emsg); + return 0; + } + dcc_error_msg("%s", wp->cw.emsg); + } + + get_hdrs(wp); + + /* log IP address and so forth that we may have collected from + * the headers or Postfix XFORWARD or XCLIENT ESTMP extension */ + thr_log_envelope(&wp->cw, 0); + + /* Check DNS blacklists for STMP client and envelope sender + * unless DNSBL checks are turned off for all of the recipients */ + if (wp->cw.cks.dnsbl) { + if (wp->cw.cks.sums[DCC_CK_IP].type == DCC_CK_IP) + dcc_client_dnsbl(wp->cw.cks.dnsbl, &wp->cw.cks.ip_addr, + wp->cw.sender_name); + if (wp->cw.mail_host[0] != '\0') + dcc_mail_host_dnsbl(wp->cw.cks.dnsbl, wp->cw.mail_host); + } + + /* copy headers from temporary file to log file */ + if (wp->cw.log_fd >= 0 && wp->cw.tmp_fd >= 0) { + if (0 > lseek(wp->cw.tmp_fd, 0, SEEK_SET)) { + thr_error_msg(&wp->cw, "rewind %s: %s", + wp->cw.tmp_nm, ERROR_STR()); + job_exit(wp); + } + for (;;) { + buflen = tmp_read(wp, buf, sizeof(buf)); + if (buflen <= 0) + break; + log_body_write(&wp->cw, buf, buflen); + } + } + + if (wp->dfgs & DFG_MISSING_BODY) + thr_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) { + if (!soc_read(wp, &wp->msg_rd)) + proxy_msg_truncated(wp); + 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 an end of line. + * So log the block of body lines + * and leave the part of a line + * ending the buffer for later. */ + 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 buffer. */ + bol = 0; + buflen = i-1; + if (buflen > 0) { + /* The buffer ends with "\rX" + * and contains text before '\r' + * Process up to the '\r' */ + break; + } + /* buffer starts with "\r" */ + if (!soc_read(wp, &wp->msg_rd)) + proxy_msg_truncated(wp); + 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 */ + log_body_write(&wp->cw, wp->msg_rd.out, buflen); + + if (wp->dfgs & DFG_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_cks_fin(&wp->cw.cks); + + LOG_CAPTION(wp, DCC_LOG_MSG_SEP); + thr_log_late(&wp->cw); + + /* check the grey and white lists */ + cmn_ask_white(&wp->cw); + + /* report spam to the DCC server in the case without a recipient + * for the ASCII protocol, + * which normally causes a query instead of a report. */ + if (!wp->cw.rcpt_st_first + && ((wp->cw.ask_st & ASK_ST_MTA_ISSPAM) + || (wp->cw.init_sws & FLTR_SW_TRAPS) + || dcc_query_only + || wp->cw.ask_st & ASK_ST_QUERY)) + ++wp->cw.tgts; + + wp->cw.header.buf[0] = '\0'; + wp->cw.header.used = 0; + if (wp->cw.tgts > wp->cw.white_tgts) { + /* Report to the DCC and add our header if allowed. + * After serious errors, act as if DCC server said not-spam + * but remove our X-DCC header */ + i = cmn_ask_dcc(&wp->cw); + if (!i && try_extra_hard) + return 0; + + } else { + /* The message is whitelisted or the MTA told us there are 0 + * recipients, so we cannot ask the DCC server. + * If it was whitelisted, add X-DCC header saying so. */ + if (wp->cw.tgts > 0) + xhdr_whitelist(&wp->cw.header); + /* Use the local target count to decide whether to log + * the mail message */ + dcc_honor_log_cnts(&wp->cw.ask_st, &wp->cw.cks, wp->cw.tgts); + } + + totals.tgts += wp->cw.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)) { + thr_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 *atchr, *addr, *cp; + int addr_len, i; + + addr = *addrp; + addr_len = *addr_lenp; + if (addr_len > RCTP_MAXNAME-1) + addr_len = *addr_lenp = RCTP_MAXNAME-1; + if (*addr == '<' && addr_len >= 2 && addr[addr_len-1] == '>') { + ++addr; + addr_len -= 2; + if (addr_len == 0) { + if (wp->dfgs & DFG_WORK_LOCK) + unlock_work(); + thr_error_msg(&wp->cw, "null recipient address"); + if (wp->dfgs & DFG_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->dfgs & DFG_WORK_LOCK) + unlock_work(); + thr_error_msg(&wp->cw, "recipient \"%s\" is too long", addr); + if (wp->dfgs & DFG_WORK_LOCK) + lock_work(); + return 0; + } + if (addr_len <= 0) { + if (wp->dfgs & DFG_WORK_LOCK) + unlock_work(); + thr_error_msg(&wp->cw, "null recipient"); + if (wp->dfgs & DFG_WORK_LOCK) + lock_work(); + return 0; + } + + if (*user_lenp) { + if (*user_lenp > RCTP_MAXNAME-1) + *user_lenp = RCTP_MAXNAME-1; + } else { + /* Use the list of local domain names to guess a user name + * from the recipient address + * Take the whole recipient name if it does not + * include a domain name */ + atchr = memchr(addr, '@', addr_len); + if (!atchr) { + *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; + if (!udom->wildcard) { + *user_lenp = i; + } else { + *user_lenp = atchr - addr; + } + return 1; + } + } + } + + return 1; +} + + + +/* The mutex must be locked */ +static RCPT_ST * +set_rcpt(WORK *wp, + const char *rcpt, int rcpt_len, + const char *user, int user_len) +{ + RCPT_ST *rcpt_st; + + rcpt_st = alloc_rcpt_st(&wp->cw, 0); + if (!rcpt_st) + return 0; + + ++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)) + thr_trace_msg(&wp->cw, "%s", wp->cw.emsg); + + return rcpt_st; +} + + + +/* 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; + RCPT_ST *rcpt_st; + + lock_work(); + wp->dfgs |= DFG_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); + rcpt_st = set_rcpt(wp, addr, addr_len, user, user_len); + if (!rcpt_st) + job_exit(wp); + + /* Don't worry if this recipient is incompatible with preceding + * recipients, because there is nothing we can do. + * We cannot reject a recipient when the ASCII protocol is used, + * but side effects of the check are required */ + cmn_compat_whitelist(&wp->cw, rcpt_st); + + wp->msg_rd.out = wp->msg_rd.next_line; + } + unlock_work(); + wp->dfgs &= ~DFG_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, DCCIF_RESULT_CHAR result_char) +{ + const char *result_str; + RCPT_ST *rcpt_st; + char *p; + + result_str = wp->cw.reply.log_result; + if (!result_str) + thr_error_msg(&wp->cw, "rejection reason undecided"); + else + thr_log_print(&wp->cw, 0, DCC_XHDR_RESULT"%s\n", result_str); + + /* 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->fgs & RCPT_FG_ISSPAM) { + *p++ = DCCIF_RCPT_REJECT; + } 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->dfgs & DFG_MTA_BODY) { + body_copy(wp); + + } else if (wp->dfgs & DFG_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) +{ +# define AOPT(s) ascii_opt_str(wp, s, LITZ(s)) + char *p; + int i; + + log_start(&wp->cw); + + 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 (AOPT(DCCIF_OPT_LOG)) { + wp->cw.ask_st |= ASK_ST_LOGIT; + continue; + } + if (AOPT(DCCIF_OPT_SPAM)) { + wp->cw.ask_st |= ASK_ST_MTA_ISSPAM; + continue; + } + if (AOPT(DCCIF_OPT_BODY)) { + wp->dfgs |= DFG_MTA_BODY; + continue; + } + if (AOPT(DCCIF_OPT_HEADER)) { + wp->dfgs |= DFG_MTA_HEADER; + continue; + } + if (AOPT(DCCIF_OPT_QUERY)) { + wp->cw.ask_st |= ASK_ST_QUERY; + continue; + } + if (AOPT(DCCIF_OPT_GREY_QUERY)) { + wp->cw.ask_st |= ASK_ST_QUERY_GREY; + continue; + } + if (AOPT(DCCIF_OPT_NO_REJECT)) { + wp->cw.action = CMN_IGNORE; + continue; + } + if (AOPT(DCCIF_OPT_RCVD_NXT) + || AOPT(DCCIF_OPT_RCVD_NEXT)) { + ++wp->parse_rcvd; + continue; + } + + thr_error_msg(&wp->cw, "unrecognized option value: \"%s\"", + wp->msg_rd.out); + job_exit(wp); + } + if ((wp->cw.ask_st & ASK_ST_MTA_ISSPAM) + && (wp->cw.ask_st & ASK_ST_QUERY)) { + thr_error_msg(&wp->cw, DCCIF_OPT_SPAM" and "DCCIF_OPT_QUERY + " are incompatible"); + wp->cw.ask_st &= ~ASK_ST_MTA_ISSPAM; + } + if ((wp->cw.ask_st & ASK_ST_MTA_ISSPAM) + && (wp->cw.ask_st & ASK_ST_QUERY_GREY)) { + thr_error_msg(&wp->cw, DCCIF_OPT_SPAM" and "DCCIF_OPT_GREY_QUERY + " are incompatible"); + wp->cw.ask_st &= ~ASK_ST_MTA_ISSPAM; + } + if (dcc_query_only + && (wp->cw.ask_st & ASK_ST_MTA_ISSPAM)) { + thr_error_msg(&wp->cw, DCCIF_OPT_SPAM" and -Q" + " are incompatible"); + wp->cw.ask_st &= ~ASK_ST_MTA_ISSPAM; + } + wp->msg_rd.out = wp->msg_rd.next_line; + + /* 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 */ + make_reply(&wp->cw.reply, &dcc_fail_reply, &wp->cw, 0); + ascii_done(wp, DCCIF_RESULT_TEMP); + } + + dcc_cks_init(&wp->cw.cks); + dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, &wp->cw, wp->cw.id); + wp->cw.cmn_fgs |= CMN_FG_LOG_EARLY; + + /* get the SMTP client IP address and host name */ + i = ascii_read_line(wp); + if (i == 0 + || !strcmp("0.0.0.0", wp->msg_rd.out) + || !strcmp("0.0.0.0\r0.0.0.0", wp->msg_rd.out)) { + /* it is absent or the SpamAssassin junk */ + wp->dfgs |= DFG_PARSE_RCVD; /* try a Received header */ + } 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); + } + /* 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)) { + thr_error_msg(&wp->cw, "unrecognized IP address \"%s\"", + wp->msg_rd.out); + } else { + /* convert IPv6 address to a canonical string */ + wp->cw.clnt_addr = wp->cw.cks.ip_addr; + dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str), + &wp->cw.clnt_addr); + } + } + 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->cw.env_from)-1) + i = ISZ(wp->cw.env_from)-1; + memcpy(wp->cw.env_from, wp->msg_rd.out, i); + wp->cw.env_from[i] = '\0'; + wp->msg_rd.out = wp->msg_rd.next_line; + + get_sender(wp); + + /* get the list of recipients from the MTA */ + get_ascii_rcpts(wp); + + if (!get_body(wp)) { + /* something wrong while collecting the message body + * such as contacting the DCC server */ + ascii_done(wp, DCCIF_RESULT_TEMP); + } + + /* get consensus of targets' wishes */ + users_process(&wp->cw); + totals.tgts_rejected += wp->cw.reject_tgts; + /* log the consensus & generate SMTP rejection message if needed */ + users_log_result(&wp->cw, 0); + + if (wp->cw.ask_st & ASK_ST_GREY_EMBARGO) { + totals.tgts_embargoed += wp->cw.tgts; + ascii_done(wp, DCCIF_RESULT_GREY); + } + + if (wp->cw.reject_tgts != 0 + || (wp->cw.tgts == 0 && (wp->cw.ask_st & ASK_ST_CLNT_ISSPAM))) { + if (wp->cw.action != CMN_IGNORE) { + if (!wp->cw.reply.log_result) + wp->cw.reply.log_result=DCC_XHDR_RESULT_REJECT; + ascii_done(wp, DCCIF_RESULT_REJECT); + } else { + wp->cw.reply.log_result = DCC_XHDR_RESULT_I_A; + ascii_done(wp, DCCIF_RESULT_OK); + } + } + + wp->cw.reply.log_result = DCC_XHDR_RESULT_ACCEPT; + ascii_done(wp, DCCIF_RESULT_OK); + +# undef AOPT +} + + + +#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_452_WLIST "452 4.5.3 "DCC_XHDR_INCOMPAT_WLIST +#define SMTP_REPLY_452_2MANY "452 4.5.3 "DCC_XHDR_TOO_MANY_RCPTS +#define SMTP_REPLY_500 "500 5.0.0 dccifd command unrecognized" +#define SMTP_REPLY_501_NO_ARG "501 5.5.4 dccifd command arg required" +#define SMTP_REPLY_501_BAD_ARG "501 5.5.1 dccifd command unrecognized" +#define SMTP_REPLY_501_RCPT "501 5.5.2 dccifd RCPT command syntax error" +#define SMTP_REPLY_501_POSTFIX "501 5.5.2 dccifd Postfix extension syntax error" +#define SMTP_REPLY_503_DATA "503 5.0.0 dccifd need RCPT" +#define SMTP_REPLY_503 "503 5.0.0 bad dccifd command sequence" + +#define SMTP_DEBUG_TRACE 3 + + +static void +smtp_trace(WORK *wp, const char *type, const char *buf, int len) +{ + if (dcc_clnt_debug < SMTP_DEBUG_TRACE) + return; + + log_start(&wp->cw); + if (len > 0 && buf[len-1] == '\n') + --len; + if (len > 0 && buf[len-1] == '\r') + --len; + thr_trace_msg(&wp->cw, "%s %s: %.*s", wp->cw.id, type, len, buf); +} + + + +/* send an SMTP reply upstream to the SMTP client of our proxy */ +static void +smtp_send_reply(WORK *wp, const char *reply, int len) +{ + if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) + smtp_trace(wp, "SMTP response", reply, 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,LITZ(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.ask_st |= ASK_ST_LOGIT; + /* depend on '\n' ending the SMTP message */ + thr_log_print(&wp->cw, 1, "%s", reply); +} + +#define SMTP_ERROR(r) smtp_reply_error(wp,SMTP_REPLY_##r,LITZ(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,LITZ(s),p,LITZ(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; + + if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) + smtp_trace(wp, "SMTP command", lp, wp->msg_rd.next_line - lp); + + 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; + } + /* finished, having matched the command */ + return ptp->verb; + } + + /* all other commands require following text separated + * from the command with whitespace, as in "Mail From:" */ + i = strspn(pp, " \t"); + if (i == 0) + continue; + pp += i; + + /* skip the initial part of the parameter, such as "From:" */ + if (ptp->parm_len) { + 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; + pp += strspn(pp, " \t"); + } + + *ppp = pp; + *pep = strpbrk(pp, " \t\r"); + return ptp->verb; + } + + *ppp = 0; + return SMTP_VERB_UNREC; +} + + + +/* reset and restart the SMTP proxy state */ +static void +proxy_rset(WORK *wp) +{ + if (wp->smtp_state == SMTP_ST_START) + return; + + cmn_clear(&wp->cw, wp, 0); + wp->dfgs &= DFG_RECYCLE; + wp->smtp_state = SMTP_ST_START; + + dcc_cks_init(&wp->cw.cks); + dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, &wp->cw, wp->cw.id); + wp->cw.cmn_fgs |= CMN_FG_LOG_EARLY; + + /* Values from the Postfix XCLIENT ESMTP extension endure for the + * entire session. Values from the XFORWARD extension or guesses from + * a HELO command are cleared at the end of the SMTP transaction. */ + if (!(wp->dfgs & DFG_XCLIENT_NAME)) + wp->cw.clnt_name[0] = '\0'; + if (wp->dfgs & DFG_XCLIENT_ADDR) { + dcc_get_ipv6_ck(&wp->cw.cks, &wp->cw.clnt_addr); + } else { + memset(&wp->cw.clnt_addr, 0, sizeof(wp->cw.clnt_addr)); + wp->cw.clnt_str[0] = '\0'; + } + if (!(wp->dfgs & DFG_XCLIENT_HELO)) + wp->cw.helo[0] = '\0'; +} + + + +/* deal with aborted SMTP sessions */ +static void +proxy_abort(WORK *wp, const char *log_msg) +{ + if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) + smtp_trace(wp, "reset", log_msg, strlen(log_msg)); + + if (wp->smtp_state != SMTP_ST_START + && wp->smtp_state != SMTP_ST_HELO + && wp->smtp_state != SMTP_ST_ERROR) { + + wp->cw.ask_st |= ASK_ST_INVALID_MSG; + + if (!(wp->cw.cmn_fgs & CMN_FG_ENV_LOGGED)) + thr_log_envelope(&wp->cw, 1); + dcc_cks_fin(&wp->cw.cks); + LOG_CAPTION(wp, "\n"DCC_LOG_MSG_SEP); + thr_log_late(&wp->cw); + cmn_ask_white(&wp->cw); + + users_process(&wp->cw); + users_log_result(&wp->cw, log_msg); + + /* create log files for -d + * and without any recipents but with "option log-all" */ + if (dcc_clnt_debug + || (wp->cw.init_sws & FLTR_SW_LOG_ALL)) + wp->cw.ask_st |= ASK_ST_LOGIT; + + if (wp->cw.ask_st & ASK_ST_LOGIT) + thr_log_print(&wp->cw, 0, + DCC_XHDR_RESULT"%s\n", log_msg); + } + proxy_rset(wp); +} + + + +/* quit a truncated message */ +static void NRATTRIB +proxy_msg_truncated(WORK *wp) +{ + if (!proxy) { + thr_error_msg(&wp->cw, "truncated message"); + } else { + proxy_abort(wp, "SMTP session aborted"); + } + job_exit(wp); +} + + + +/* pass a line from the downstream proxy to our upstream */ +static void +smtp_pass_line(WORK *wp) +{ + int len; + + len = wp->reply_in.next_line - wp->reply_in.out; + if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) + smtp_trace(wp, "pass SMTP response", wp->reply_in.out, len); + + 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; +} + + + +static int /* -1 or 1st digit of response */ +smtp_get_resp(WORK *wp, + u_char pass, /* 1=pass it upstream */ + char *logbuf, /* log response here */ + int logbuflen) +{ + int len; + char dig, cont; + + for (;;) { + if (!msg_read_line(wp, &wp->reply_in)) { + if (pass) + job_exit(wp); + return -1; + } + len = wp->reply_in.next_line - wp->reply_in.out; + if (len < 5) { + thr_error_msg(&wp->cw, "short SMTP response" + " \"%s\" from proxy server", + wp->reply_in.out); + return -1; + } + 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'; + thr_error_msg(&wp->cw, "unrecognized SMTP response" + " \"%s\" from proxy", + wp->reply_in.out); + return -1; + } + + if (logbuflen != 0) { + if (logbuflen > len) + logbuflen = len; + memcpy(logbuf, wp->reply_in.out, logbuflen); + while (logbuflen > 0 + && (logbuf[logbuflen-1] == '\r' + || logbuf[logbuflen-1] == '\n')) + --logbuflen; + logbuf[logbuflen] = '\0'; + + /* log only the first line */ + logbuflen = 0; + } + + if (pass) { + /* pass the next line upstream */ + smtp_pass_line(wp); + if (cont == ' ') + return dig; + } else { + /* we don't like multi-line responses */ + if (cont == ' ') + return dig; + + *wp->reply_in.next_line = '\0'; + thr_error_msg(&wp->cw, "unacceptable multi-line" + " SMTP response" + " \"%s\" from proxy", + wp->reply_in.out); + return -1; + } + } +} + + + +/* pass an SMTP command from upstream or the SMTP client of our proxy + * downstream to the SMTP server of our proxy. + * Then relay the SMTP server's response upstream to the SMTP client */ +static u_char /* 0=server was unhappy */ +smtp_pass_cmd(WORK *wp, + const char *reply, /* send this response upstream if */ + int reply_len, /* if downstream is happy */ + char *logbuf, /* logbuf response here */ + int logbuflen) +{ + int len, result; + + /* 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 */ + result = smtp_get_resp(wp, 1, logbuf, logbuflen); + if (result < 0) + job_exit(wp); + return (result == reply[0]); +} + +#define SMTP_PASS_CMD(r) smtp_pass_cmd(wp,SMTP_REPLY_##r,LITZ(SMTP_REPLY_##r), \ + 0, 0) + + + +/* parse Postfix XFORWARD and XCLIENT commands + * see http://www.postfix.org/XCLIENT_README.html + * and http://www.postfix.org/XFORWARD_README.html + * Depend on the Postfix downstream to answer EHLO with XFORWRD */ +static void +smtp_xpostfix(WORK *wp, + VERB_SMTP verb, /* SMTP_VERB_XFORWARD or _XCLIENT */ + const char *parm) +{ + const char *val, *end_parm; + char addr[INET6_ADDRSTRLEN+1]; + int i; + + wp->smtp_state = SMTP_ST_HELO; + + for (; ; parm = end_parm + strspn(end_parm, " \t")) { + if (*parm == '\r' || *parm == '\n') { + SMTP_PASS_CMD(250_POSTFIX); + break; + } + + /* find the value */ + val = strpbrk(parm, "= \t\r\n"); + if (*val++ != '=') { + SMTP_REPLY(501_POSTFIX); + break; + } + + end_parm = strpbrk(val, " \t\r\n"); + + if (!CLITCMP(parm, "NAME=")) { + if (!CLITCMP(val, "[UNAVAILABLE]") + || !CLITCMP(val, "[TEMPUNAVAIL]")) { + wp->cw.clnt_name[0] = '\0'; + continue; + } + i = min(ISZ(wp->cw.clnt_name)-1, end_parm-val); + memcpy(wp->cw.clnt_name, val, i); + wp->cw.clnt_name[i] = '\0'; + if (verb == SMTP_VERB_XCLIENT) + wp->dfgs |= DFG_XCLIENT_NAME; + else + wp->dfgs &= ~DFG_XCLIENT_NAME; + continue; + } + + if (!CLITCMP(parm, "ADDR=")) { + if (!CLITCMP(val, "[UNAVAILABLE]") + || !CLITCMP(val, "[TEMPUNAVAIL]")) { + wp->cw.clnt_str[0] = '\0'; + continue; + } + if (!CLITCMP(val, "IPV6")) + val += LITZ("IPV6"); + i = min(ISZ(addr)-1, end_parm-val); + memcpy(addr, val, i); + addr[i] = '\0'; + /* try to convert ASCCI representation of IP address to + * a canonical form and to a checksum */ + if (!dcc_get_str_ip_ck(&wp->cw.cks, addr)) { + thr_error_msg(&wp->cw, + "unrecognized IP address \"%s\"", + addr); + wp->cw.clnt_str[0] = '\0'; + continue; + } + wp->cw.clnt_addr = wp->cw.cks.ip_addr; + dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str), + &wp->cw.clnt_addr); + if (verb == SMTP_VERB_XCLIENT) { + wp->dfgs |= DFG_XCLIENT_ADDR; + } else { + wp->dfgs &= ~DFG_XCLIENT_ADDR; + } + continue; + } + + if (!CLITCMP(parm, "HELO=")) { + if (!CLITCMP(val, "[UNAVAILABLE]") + || !CLITCMP(val, "[TEMPUNAVAIL]")) { + wp->cw.helo[0] = '\0'; + } else { + get_helo(wp, val, end_parm-val); + } + if (verb == SMTP_VERB_XCLIENT) + wp->dfgs |= DFG_XCLIENT_HELO; + else + wp->dfgs &= ~DFG_XCLIENT_HELO; + continue; + } + } + + wp->msg_rd.out = wp->msg_rd.next_line; +} + + + +/* parse SMTP Rcpt_To command */ +static u_char /* 1=now seen >=1 good Rcpt command*/ +get_smtp_rcpt(WORK *wp, + const char *path, + const char *epath) +{ + const char *user; + int path_len, user_len; + RCPT_ST *rcpt_st; + + 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; + } + + lock_work(); + wp->dfgs |= DFG_WORK_LOCK; + rcpt_st = set_rcpt(wp, path, path_len, user, user_len); + unlock_work(); + wp->dfgs &= ~DFG_WORK_LOCK; + if (!rcpt_st) { + SMTP_REPLY(452_2MANY); + wp->msg_rd.out = wp->msg_rd.next_line; + return 0; + } + + /* if this recipient is incompatible with preceding recipients + * then reject it and remember to put something into the log */ + if (!cmn_compat_whitelist(&wp->cw, rcpt_st)) { + --wp->cw.tgts; + SMTP_REPLY(452_WLIST); + wp->msg_rd.out = wp->msg_rd.next_line; + BUFCPY(rcpt_st->rej_msg, SMTP_REPLY_452_WLIST); + rcpt_st->rej_result = DCC_XHDR_INCOMPAT_WLIST; + rcpt_st->fgs |= RCPT_FG_REJ_FILTER; + return 0; + } + + /* Try to pass the command in the buffer downstream. + * Forget this recipient if it is not good enough downstream. + * Let the downstream proxy do its own logging. + * That way we need save only one bit instead of an SMTP + * rejection message for our eventual per-user log file. */ + if (!smtp_pass_cmd(wp, SMTP_REPLY_250_RCPT, LITZ(SMTP_REPLY_250_RCPT), + rcpt_st->rej_msg, sizeof(rcpt_st->rej_msg))) { + --wp->cw.tgts; + wp->msg_rd.out = wp->msg_rd.next_line; + rcpt_st->rej_result = rcpt_st->rej_msg; + rcpt_st->rej_result += strspn(rcpt_st->rej_result, + "0123456789. "); + if (rcpt_st->rej_msg[0] != '4') + ++wp->cw.mta_rej_tgts; + wp->cw.ask_st |= ASK_ST_LOGIT; + rcpt_st->fgs |= RCPT_FG_REJ_FILTER; + return 0; + } + + /* there was no problem if the downstream was happy */ + rcpt_st->rej_msg[0] = '\0'; + + wp->msg_rd.out = wp->msg_rd.next_line; + return 1; +} + + + +static void +smtp_data_abort(WORK *wp) +{ + /* After a rejection Postfix wants a before-queue proxy to + * "abort the connnection" to the SMTP server downstream. + * That might mean a simple TCP shutdown, but for testing with + * sendmail, send an SMTP RSET command. */ + if (proxy_out_family != AF_UNSPEC) { + buf_write(wp, &wp->msg_wt, "RSET\r\n", LITZ("RSET\r\n")); + buf_write_flush(wp, &wp->msg_wt); + /* sendmail will respond, but what about Postfix? */ + if (smtp_get_resp(wp, 0, 0, 0) >= 0 + && dcc_clnt_debug >= SMTP_DEBUG_TRACE) { + smtp_trace(wp, "ignore response to RSET", + wp->reply_in.out, + wp->reply_in.next_line + - wp->reply_in.out); + wp->reply_in.out = wp->reply_in.next_line; + } + } +} + + + +/* We are rejecting the transaction with our generated reply */ +static void +smtp_trans_reject(WORK *wp) +{ + if (dcc_clnt_debug >= SMTP_DEBUG_TRACE) + thr_trace_msg(&wp->cw, "%s SMTP response: %s %s %s", + wp->cw.id, + wp->cw.reply.rcode, wp->cw.reply.xcode, + wp->cw.reply.str); + + if (wp->proxy_in_soc >= 0) { + buf_write(wp, &wp->reply_out, + wp->cw.reply.rcode, strlen(wp->cw.reply.rcode)); + buf_write(wp, &wp->reply_out, " ", 1); + buf_write(wp, &wp->reply_out, + wp->cw.reply.xcode, strlen(wp->cw.reply.xcode)); + buf_write(wp, &wp->reply_out, " ", 1); + buf_write(wp, &wp->reply_out, + wp->cw.reply.str, strlen(wp->cw.reply.str)); + buf_write(wp, &wp->reply_out, "\r\n", 2); + buf_write_flush(wp, &wp->reply_out); + log_smtp_reply(&wp->cw); + } + + if (wp->proxy_out_soc >= 0) + smtp_data_abort(wp); +} + + + +static void NRATTRIB +smtp_temp_fail(WORK *wp) +{ + make_reply(&wp->cw.reply, &dcc_fail_reply, &wp->cw, 0); + smtp_trans_reject(wp); + job_exit(wp); +} + + + +/* Send the mail message to the SMTP server for our proxy. */ +static const char * +smtp_data_send(WORK *wp) +{ + int class; + int len; + const char *result; + + /* First send the DATA command. */ + buf_write(wp, &wp->msg_wt, "DATA\r\n", LITZ("DATA\r\n")); + buf_write_flush(wp, &wp->msg_wt); + + /* the server should respond with a 350 result */ + class = smtp_get_resp(wp, 0, 0, 0); + if (class < 0) + job_exit(wp); + + if (class == '3') { + wp->reply_in.out = wp->reply_in.next_line; + + /* send the mail message to the SMTP server */ + body_copy(wp); + buf_write(wp, &wp->msg_wt, ".\r\n", 3); + buf_write_flush(wp, &wp->msg_wt); + + /* see what it has to say */ + class = smtp_get_resp(wp, 0, 0, 0); + + } else if (class != '4' && class != '5') { + /* or quit if response to DATA was not 4yz or 5yz */ + thr_error_msg(&wp->cw, "unrecognized SMTP response" + " \"%s\" from proxy to DATA command", + wp->reply_in.out); + job_exit(wp); + } + + if (class == '2') { + result = DCC_XHDR_RESULT_ACCEPT; + } else { + len = wp->reply_in.next_line - wp->reply_in.out; + while (len > 0 && (wp->reply_in.out[len-1] == '\r' + || wp->reply_in.out[len-1] == '\n')) + --len; + if (len > ISZ(wp->cw.reply.str)-2) + len = ISZ(wp->cw.reply.str)-2; + memcpy(wp->cw.reply.str_buf, wp->reply_in.out, len); + wp->cw.reply.str_buf[len++] = '\n'; + wp->cw.reply.str_buf[len] = '\0'; + wp->cw.reply.str = wp->cw.reply.str_buf; + thr_log_print(&wp->cw, 0, + "SMTP server "DCC_XHDR_REJ_DATA_MSG"%.*s", + len, wp->cw.reply.str); + if (dcc_clnt_debug) + wp->cw.ask_st |= ASK_ST_LOGIT; + result = "rejected by SMTP server"; + } + + /* pass SMTP server's response to the SMTP client */ + smtp_pass_line(wp); + + return result; +} + + + +static void +smtp_data(WORK *wp) +{ + const char *global_result, *user_result; + + if (!wp->cw.num_rcpts) { + /* never got SMTP DATA command */ + 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)) { + /* something went wrong while collecting the message body + * such as contacting the DCC server */ + smtp_temp_fail(wp); + return; + } + + /* get consensus of targets' wishes */ + users_process(&wp->cw); + + if (wp->cw.ask_st & ASK_ST_GREY_EMBARGO) { + totals.tgts_embargoed += wp->cw.tgts; + smtp_trans_reject(wp); + global_result = wp->cw.reply.log_result; + user_result = global_result; + + } else if (wp->cw.reject_tgts != 0) { + switch (wp->cw.action) { + case CMN_IGNORE: + totals.tgts_ignored += wp->cw.reject_tgts; + ++totals.msgs_spam; + if (proxy_out_family == AF_UNSPEC) { + SMTP_REPLY(250_DATA); + global_result = DCC_XHDR_RESULT_I_A; + } else { + global_result = smtp_data_send(wp); + } + user_result = global_result; + break; + + case CMN_DISCARD: + totals.tgts_discarded += wp->cw.reject_tgts; + ++totals.msgs_spam; + /* discard the message by aborting the downstream + * SMTP session and telling the upstream 250-OK */ + if (proxy_out_family != AF_UNSPEC) + smtp_data_abort(wp); + SMTP_REPLY(250_DATA); + global_result = DCC_XHDR_RESULT_DISCARD; + user_result = global_result; + break; + + case CMN_REJECT: + default: + totals.tgts_rejected += wp->cw.reject_tgts; + ++totals.msgs_spam; + smtp_trans_reject(wp); + global_result = wp->cw.reply.log_result; + user_result = 0; + break; + } + + } else if (proxy_out_family == AF_UNSPEC) { + SMTP_REPLY(250_DATA); + global_result = DCC_XHDR_RESULT_ACCEPT; + user_result = 0; + + } else { + global_result = smtp_data_send(wp); + user_result = 0; + } + + /* log the consensus & generate SMTP rejection message if needed */ + users_log_result(&wp->cw, user_result); + thr_log_print(&wp->cw, 0, DCC_XHDR_RESULT"%s\n", global_result); + + proxy_rset(wp); +} + + + +/* use a subset of SMTP to talk to an MTA */ +static void NRATTRIB +proxy_job(WORK *wp) +{ + char str[DCC_SU2STR_SIZE]; + DCC_SOCKU su; + int out_soc; + VERB_SMTP verb; + const char *parmp, *eparmp; + 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_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; + + if (proxy_out_family == AF_UNSPEC) { + /* single-ended */ + ; + + } else if (proxy_out_family == AF_UNIX) { + out_soc = socket(AF_UNIX, SOCK_STREAM, 0); + if (out_soc < 0) { + thr_error_msg(&wp->cw, "proxy_out socket(AF_UNIX): %s", + ERROR_STR()); + smtp_temp_fail(wp); + } + if (0 > connect(out_soc, + (struct sockaddr *)&proxy_out_sun, + sizeof(listen_sun))) { + thr_error_msg(&wp->cw, + "proxy_out connect(AF_UNIX,%s): %s", + proxy_out_sun.sun_path, + ERROR_STR()); + close(out_soc); + smtp_temp_fail(wp); + } + if (!set_soc(wp->cw.emsg, out_soc, AF_UNIX, + "proxy output")) { + thr_error_msg(&wp->cw, "%s", wp->cw.emsg); + close(out_soc); + smtp_temp_fail(wp); + } + wp->proxy_out_soc = out_soc; + wp->dfgs |= DFG_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; + } + out_soc = socket(su.sa.sa_family, SOCK_STREAM, 0); + if (out_soc < 0) { + thr_error_msg(&wp->cw, "proxy_out socket(%s): %s", + dcc_su2str(str, sizeof(str), &su), + ERROR_STR()); + close(out_soc); + smtp_temp_fail(wp); + } + if (0 > connect(out_soc, &su.sa, DCC_SU_LEN(&su))) { + thr_error_msg(&wp->cw, + "proxy_out connect(%s): %s", + dcc_su2str(str, sizeof(str), &su), + ERROR_STR()); + close(out_soc); + smtp_temp_fail(wp); + } + if (!set_soc(wp->cw.emsg, out_soc, su.sa.sa_family, + "proxy output")) { + thr_error_msg(&wp->cw, "%s", wp->cw.emsg); + close(out_soc); + smtp_temp_fail(wp); + } + wp->proxy_out_soc = out_soc; + wp->dfgs |= DFG_MTA_BODY; + } + + /* 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 */ + smtp_temp_fail(wp); + } + dcc_cks_init(&wp->cw.cks); + dcc_dnsbl_init(&wp->cw.cks, wp->cw.dcc_ctxt, &wp->cw, wp->cw.id); + wp->cw.cmn_fgs |= CMN_FG_LOG_EARLY; + + /* pass the banner upstream */ + wp->smtp_state = SMTP_ST_START; + SMTP_PASS_CMD(220); + + /* assume for now that the IP address is the SMTP client to our proxy */ + if (wp->proxy_in_su.sa.sa_family != AF_UNIX) { + if (wp->proxy_in_su.sa.sa_family == AF_INET) { + dcc_ipv4toipv6(&wp->cw.clnt_addr, + wp->proxy_in_su.ipv4.sin_addr); + } else { + wp->cw.clnt_addr = (wp->proxy_in_su.ipv6.sin6_addr); + } + dcc_ipv6tostr(wp->cw.clnt_str, sizeof(wp->cw.clnt_str), + &wp->cw.clnt_addr); + } + + for (;;) { + buf_write_flush(wp, &wp->reply_out); + if (!msg_read_line(wp, &wp->msg_rd)) { + proxy_abort(wp, "SMTP session aborted"); + 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_CMD(500); + wp->msg_rd.out = wp->msg_rd.next_line; + continue; + case SMTP_VERB_QUIT: + SMTP_PASS_CMD(221); + proxy_abort(wp, "SMTP session aborted with QUIT"); + job_exit(wp); + case SMTP_VERB_RSET: + SMTP_PASS_CMD(250_RSET); + proxy_abort(wp, "SMTP transaction aborted with RSET"); + wp->msg_rd.out = wp->msg_rd.next_line; + continue; + case SMTP_VERB_HELO: + proxy_abort(wp, "SMTP transaction aborted with HELO"); + wp->smtp_state = SMTP_ST_HELO; + /* save helo value in case there is no XPOSTFIX */ + if (SMTP_PASS_CMD(250_HELO) + && wp->cw.helo[0] == '\0' + && !(wp->dfgs & DFG_XCLIENT_HELO)) + get_helo(wp, parmp, eparmp - parmp); + wp->msg_rd.out = wp->msg_rd.next_line; + continue; + case SMTP_VERB_XCLIENT: + case SMTP_VERB_XFORWARD: + case SMTP_VERB_MAIL: + case SMTP_VERB_RCPT: + case SMTP_VERB_DATA: + break; + } + + switch (wp->smtp_state) { + case SMTP_ST_START: /* expecting HELO or perhaps MAIL */ + case SMTP_ST_HELO: /* seen HELO */ + if (verb == SMTP_VERB_XCLIENT + || verb == SMTP_VERB_XFORWARD) { + smtp_xpostfix(wp, verb, parmp); + continue; + } + if (verb == SMTP_VERB_MAIL) { + /* This might be a second message for the + * connection. + * Get sender, start logging, etc. at + * the Mail_From command. */ + if (SMTP_PASS_CMD(250_MAIL)) { + log_start(&wp->cw); + i = eparmp - parmp; + if (i > ISZ(wp->cw.env_from)-1) + i = ISZ(wp->cw.env_from)-1; + memcpy(wp->cw.env_from, parmp, i); + wp->cw.env_from[i] = '\0'; + wp->smtp_state = SMTP_ST_TRANS; + } + wp->msg_rd.out = wp->msg_rd.next_line; + get_sender(wp); + continue; + } + 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 */ + if (verb == SMTP_VERB_RCPT) { + get_smtp_rcpt(wp, parmp, eparmp); + continue; + } + if (verb == SMTP_VERB_DATA) { + smtp_data(wp); + continue; + } + break; + + case SMTP_ST_ERROR: /* no transaction until RSET or HELO */ + break; + } + + /* reject anything out of sequence */ + SMTP_ERROR(503); + } +} + + + +/* start a new connection to an MTA */ +static void * NRATTRIB +job_start(void *wp0) +{ + WORK *wp = wp0; + + /* working threads do not deal with signals */ + clnt_sigs_off(0); + + if (proxy) + proxy_job(wp); + else + ascii_job(wp); +}