Plan 9 from Bell Labs’s /usr/web/sources/patch/sorry/smtpdspam/smtpd.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#include "common.h"
#include "smtpd.h"
#include "smtp.h"
#include <ctype.h>
#include <ip.h>
#include <ndb.h>
#include <mp.h>
#include <libsec.h>
#include <auth.h>
#include "../smtp/y.tab.h"

char	*me;
char	*him="";
char	*dom;
process	*pp;
String	*mailer;
NetConnInfo *nci;

int	filterstate = ACCEPT;
int	trusted;
int	logged;
int	rejectcount;
int	hardreject;

Biobuf	bin;

int	debug;
int	Dflag;
int	fflag;
int	gflag;
int	qflag;
int	rflag;
int	sflag;
int	authenticate;
int	authenticated;
int	passwordinclear;
char	*tlscert;

uchar	rsysip[IPaddrlen];

List	senders;
List	rcvers;

char	pipbuf[ERRMAX];
char	*piperror;

String*	mailerpath(char*);
int	pipemsg(int*);
int	rejectcheck(void);
String*	startcmd(void);

static int
catchalarm(void *a, char *msg)
{
	int rv = 1;

	USED(a);

	/* log alarms but continue */
	if(strstr(msg, "alarm")){
		if(senders.first && rcvers.first)
			syslog(0, "smtpd", "note: %s->%s: %s",
				s_to_c(senders.first->p),
				s_to_c(rcvers.first->p), msg);
		else
			syslog(0, "smtpd", "note: %s", msg);
		rv = 0;
	}

	/* kill the children if there are any */
	if(pp)
		syskillpg(pp->pid);

	return rv;
}

/* override string error functions to do something reasonable */
void
s_error(char *f, char *status)
{
	char errbuf[Errlen];

	errbuf[0] = 0;
	rerrstr(errbuf, sizeof(errbuf));
	if(f && *f)
		reply("452 4.3.0 out of memory %s: %s\r\n", f, errbuf);
	else
		reply("452 4.3.0 out of memory %s\r\n", errbuf);
	syslog(0, "smtpd", "++Malloc failure %s [%s]", him, nci->rsys);
	exits(status);
}

static void
usage(void)
{
	fprint(2,
	  "usage: smtpd [-adDfghprs] [-c cert] [-k ip] [-m mailer] [-n net]\n");
	exits("usage");
}

void
main(int argc, char **argv)
{
	char *netdir;
	char buf[1024];

	netdir = nil;
	quotefmtinstall();
	fmtinstall('I', eipfmt);
	ARGBEGIN{
	case 'a':
		authenticate = 1;
		break;
	case 'c':
		tlscert = EARGF(usage());
		break;
	case 'D':
		Dflag++;
		break;
	case 'd':
		debug++;
		break;
	case 'f':				/* disallow relaying */
		fflag = 1;
		break;
	case 'g':
		gflag = 1;
		break;
	case 'h':				/* default domain name */
		dom = EARGF(usage());
		break;
	case 'k':				/* prohibited ip address */
		addbadguy(EARGF(usage()));
		break;
	case 'm':				/* set mail command */
		mailer = mailerpath(EARGF(usage()));
		break;
	case 'n':				/* log peer ip address */
		netdir = EARGF(usage());
		break;
	case 'p':
		passwordinclear = 1;
		break;
	case 'q':
		qflag = 1;		/* don't log invalid hello */
		break;
	case 'r':
		rflag = 1;			/* verify sender's domain */
		break;
	case 's':				/* save blocked messages */
		sflag = 1;
		break;
	case 't':
		fprint(2, "%s: the -t option is no longer supported, see -c\n",
			argv0);
		tlscert = "/sys/lib/ssl/smtpd-cert.pem";
		break;
	default:
		usage();
	}ARGEND;

	nci = getnetconninfo(netdir, 0);
	if(nci == nil)
		sysfatal("can't get remote system's address");
	parseip(rsysip, nci->rsys);

	if(mailer == nil)
		mailer = mailerpath("send");

	if(debug){
		close(2);
		snprint(buf, sizeof(buf), "%s/smtpd.db", UPASLOG);
		if (open(buf, OWRITE) >= 0) {
			seek(2, 0, 2);
			fprint(2, "%d smtpd %s\n", getpid(), thedate());
		} else
			debug = 0;
	}
	getconf();
	if(isbadguy())
		exits("");
	Binit(&bin, 0, OREAD);

	if (chdir(UPASLOG) < 0)
		syslog(0, "smtpd", "no %s: %r", UPASLOG);
	me = sysname_read();
	if(dom == 0 || dom[0] == 0)
		dom = domainname_read();
	if(dom == 0 || dom[0] == 0)
		dom = me;
	sayhi();
	parseinit();

	/* allow 45 minutes to parse the header */
	atnotify(catchalarm, 1);
	alarm(45*60*1000);
	zzparse();
	exits(0);
}

void
listfree(List *l)
{
	Link *lp, *next;

	for(lp = l->first; lp; lp = next){
		next = lp->next;
		s_free(lp->p);
		free(lp);
	}
	l->first = l->last = 0;
}

void
listadd(List *l, String *path)
{
	Link *lp;

	lp = (Link *)malloc(sizeof *lp);
	lp->p = path;
	lp->next = 0;

	if(l->last)
		l->last->next = lp;
	else
		l->first = lp;
	l->last = lp;
}

#define	SIZE	4096

int
reply(char *fmt, ...)
{
	int n;
	char buf[SIZE], *out;
	va_list arg;

	va_start(arg, fmt);
	out = vseprint(buf, buf+SIZE, fmt, arg);
	va_end(arg);

	n = (long)(out - buf);
	if(debug) {
		seek(2, 0, 2);
		write(2, buf, n);
	}
	write(1, buf, n);
	return n;
}

void
reset(void)
{
	if(rejectcheck())
		return;
	listfree(&rcvers);
	listfree(&senders);
	if(filterstate != DIALUP){
		logged = 0;
		filterstate = ACCEPT;
	}
	reply("250 2.0.0 ok\r\n");
}

void
sayhi(void)
{
	reply("220 %s ESMTP\r\n", dom);
}

int
dnsexists(char *d)
{
	int r;
	Ndbtuple *t;

	r = -1;
	if(t = dnsquery(nci->root, d, "any"))
		r = 0;
	ndbfree(t);
	return r;
}

/*
 * make callers from class A networks infested by spammers
 * wait longer.
 */

static char netaspam[256] = {
	[58]	1,
	[66]	1,
	[71]	1,

	[76]	1,
	[77]	1,
	[78]	1,
	[79]	1,
	[80]	1,
	[81]	1,
	[82]	1,
	[83]	1,
	[84]	1,
	[85]	1,
	[86]	1,
	[87]	1,
	[88]	1,
	[89]	1,

	[190]	1,
	[201]	1,
	[217]	1,
};

static int
delaysecs(void)
{
	if (netaspam[rsysip[0]])
		return 60;
	return 15;
}

void
hello(String *himp, int extended)
{
	char **mynames;
	char *ldot, *rdot;

	him = s_to_c(himp);
	if(!qflag)
		syslog(0, "smtpd", "%s from %s as %s", extended? "ehlo": "helo",
			nci->rsys, him);
	if(rejectcheck())
		return;

	if (strchr(him, '.') && nci && !trusted && fflag &&
	    strcmp(nci->rsys, nci->lsys) != 0){
		/*
		 * We don't care if he lies about who he is, but it is
		 * not okay to pretend to be us.  Many viruses do this,
		 * just parroting back what we say in the greeting.
		 */
		if(strcmp(him, dom) == 0)
			goto Liarliar;
		for(mynames = sysnames_read(); mynames && *mynames; mynames++){
			if(cistrcmp(*mynames, him) == 0){
Liarliar:
				if(!qflag)
					syslog(0, "smtpd", "Hung up on %s; "
						"claimed to be %s", nci->rsys, him);
				reply("554 5.7.0 Liar!\r\n");
				exits("client pretended to be us");
				return;
			}
		}
	}

	/*
	 * it is unacceptable to claim any string that doesn't look like
	 * a domain name (e.g., has at least one dot in it), but
	 * Microsoft mail client software gets this wrong, so let trusted
	 * (local) clients omit the dot.
	 */
	rdot = strrchr(him, '.');
	if (rdot && rdot[1] == '\0') {
		*rdot = '\0';			/* clobber trailing dot */
		rdot = strrchr(him, '.');	/* try again */
	}
	if (!trusted && rdot == nil)
		goto Liarliar;
	/*
	 * Reject obviously bogus domains and those reserved by RFC 2606.
	 */
	if (rdot == nil)
		rdot = him;
	else
		rdot++;
	if (cistrcmp(rdot, "localdomain") == 0 ||
	    cistrcmp(rdot, "localhost") == 0 ||
	    cistrcmp(rdot, "local") == 0 ||
	    cistrcmp(rdot, "example") == 0 ||
	    cistrcmp(rdot, "invalid") == 0 ||
	    cistrcmp(rdot, "lan") == 0 ||
	    cistrcmp(rdot, "test") == 0)
		goto Liarliar;			/* bad top-level domain */
	/* check second-level RFC 2606 domains: example\.(com|net|org) */
	if (rdot != him)
		*--rdot = '\0';
	ldot = strrchr(him, '.');
	if (rdot != him)
		*rdot = '.';
	if (ldot == nil)
		ldot = him;
	else
		ldot++;
	if (cistrcmp(ldot, "example.com") == 0 ||
	    cistrcmp(ldot, "example.net") == 0 ||
	    cistrcmp(ldot, "example.org") == 0)
		goto Liarliar;

	/*
	 * similarly, if the claimed domain is not an address-literal,
	 * require at least one letter, which there will be in
	 * at least the last component (e.g., .com, .net) if it's real.
	 * this rejects non-address-literal IP addresses,
	 * among other bogosities.
	 */
	if (!trusted && (1 || him[0] != '[')) {
		char *p;

		for (p = him; *p != '\0'; p++)
			if (isascii(*p) && isalpha(*p))
				break;
		if (*p == '\0')
			goto Liarliar;
	}

	/* finally, just insist on an resolvable domain name */
	if(!trusted && dnsexists(him) == -1)
		goto Liarliar;


	if(strchr(him, '.') == 0 && nci != nil && strchr(nci->rsys, '.') != nil)
		him = nci->rsys;

	if(qflag)
		syslog(0, "smtpd", "%s from %s as %s", extended? "ehlo": "helo",
			nci->rsys, him);
	if(Dflag)
		sleep(delaysecs()*1000);
	reply("250%c%s you are %s\r\n", extended ? '-' : ' ', dom, him);
	if (extended) {
		reply("250-ENHANCEDSTATUSCODES\r\n");	/* RFCs 2034 and 3463 */
		if(tlscert != nil)
			reply("250-STARTTLS\r\n");
		if (passwordinclear)
			reply("250 AUTH CRAM-MD5 PLAIN LOGIN\r\n");
		else
			reply("250 AUTH CRAM-MD5\r\n");
	}
}

void
sender(String *path)
{
	String *s;
	static char *lastsender;

	if(rejectcheck())
		return;
	if (authenticate && !authenticated) {
		rejectcount++;
		reply("530 5.7.0 Authentication required\r\n");
		return;
	}
	if(him == 0 || *him == 0){
		rejectcount++;
		reply("503 Start by saying HELO, please.\r\n", s_to_c(path));
		return;
	}

	/* don't add the domain onto black holes or we will loop */
	if(strchr(s_to_c(path), '!') == 0 && strcmp(s_to_c(path), "/dev/null") != 0){
		s = s_new();
		s_append(s, him);
		s_append(s, "!");
		s_append(s, s_to_c(path));
		s_terminate(s);
		s_free(path);
		path = s;
	}
	if(shellchars(s_to_c(path))){
		rejectcount++;
		reply("501 5.1.3 Bad character in sender address %s.\r\n",
			s_to_c(path));
		return;
	}

	/*
	 * if the last sender address resulted in a rejection because the sending
	 * domain didn't exist and this sender has the same domain, reject
	 * immediately.
	 */
	if(lastsender){
		if (strncmp(lastsender, s_to_c(path), strlen(lastsender)) == 0){
			filterstate = REFUSED;
			rejectcount++;
			reply("554 5.1.8 Sender domain must exist: %s\r\n",
				s_to_c(path));
			return;
		}
		free(lastsender);	/* different sender domain */
		lastsender = 0;
	}

	/*
	 * see if this ip address, domain name, user name or account is blocked
	 */
	filterstate = blocked(path);

	logged = 0;
	listadd(&senders, path);
	reply("250 2.0.0 sender is %s\r\n", s_to_c(path));
}

enum { Rcpt, Domain, Ntoks };

typedef struct Sender Sender;
struct Sender {
	Sender	*next;
	char	*rcpt;
	char	*domain;
};
static Sender *sendlist, *sendlast;

static int
rdsenders(void)
{
	int lnlen, nf, ok = 1;
	char *line, *senderfile;
	char *toks[Ntoks];
	Biobuf *sf;
	Sender *snd;
	static int beenhere = 0;

	if (beenhere)
		return 1;
	beenhere = 1;

	/*
	 * we're sticking with a system-wide sender list because
	 * per-user lists would require fully resolving recipient
	 * addresses to determine which users they correspond to
	 * (barring exploiting syntactic conventions).
	 */
	senderfile = smprint("%s/senders", UPASLIB);
	sf = Bopen(senderfile, OREAD);
	free(senderfile);
	if (sf == nil)
		return 1;
	while ((line = Brdline(sf, '\n')) != nil) {
		if (line[0] == '#' || line[0] == '\n')
			continue;
		lnlen = Blinelen(sf);
		line[lnlen-1] = '\0';		/* clobber newline */
		nf = tokenize(line, toks, nelem(toks));
		if (nf != nelem(toks))
			continue;		/* malformed line */

		snd = malloc(sizeof *snd);
		if (snd == nil)
			sysfatal("out of memory: %r");
		memset(snd, 0, sizeof *snd);
		snd->next = nil;

		if (sendlast == nil)
			sendlist = snd;
		else
			sendlast->next = snd;
		sendlast = snd;
		snd->rcpt = strdup(toks[Rcpt]);
		snd->domain = strdup(toks[Domain]);
	}
	Bterm(sf);
	return ok;
}

/*
 * read (recipient, sender's DNS) pairs from /mail/lib/senders.
 * Only allow mail to recipient from any of sender's IPs.
 * A recipient not mentioned in the file is always permitted.
 */
static int
senderok(char *rcpt)
{
	int mentioned = 0, matched = 0;
	uchar dnsip[IPaddrlen];
	Sender *snd;
	Ndbtuple *nt, *next, *first;

	rdsenders();
	for (snd = sendlist; snd != nil; snd = snd->next) {
		if (strcmp(rcpt, snd->rcpt) != 0)
			continue;
		/*
		 * see if this domain's ips match nci->rsys.
		 * if not, perhaps a later entry's domain will.
		 */
		mentioned = 1;
		if (parseip(dnsip, snd->domain) != -1 &&
		    memcmp(rsysip, dnsip, IPaddrlen) == 0)
			return 1;
		/*
		 * NB: nt->line links form a circular list(!).
		 * we need to make one complete pass over it to free it all.
		 */
		first = nt = dnsquery(nci->root, snd->domain, "ip");
		if (first == nil)
			continue;
		do {
			if (strcmp(nt->attr, "ip") == 0 &&
			    parseip(dnsip, nt->val) != -1 &&
			    memcmp(rsysip, dnsip, IPaddrlen) == 0)
				matched = 1;
			next = nt->line;
			free(nt);
			nt = next;
		} while (nt != first);
	}
	if (matched)
		return 1;
	else
		return !mentioned;
}

void
receiver(String *path)
{
	char *sender, *rcpt;

	if(rejectcheck())
		return;
	if(him == 0 || *him == 0){
		rejectcount++;
		reply("503 Start by saying HELO, please\r\n");
		return;
	}
	if(senders.last)
		sender = s_to_c(senders.last->p);
	else
		sender = "<unknown>";

	if(!recipok(s_to_c(path))){
		rejectcount++;
		syslog(0, "smtpd",
		 "Disallowed %s (%s/%s) to blocked, unknown or invalid name %s",
			sender, him, nci->rsys, s_to_c(path));
		reply("550 5.1.1 %s ... user unknown\r\n", s_to_c(path));
		return;
	}
	rcpt = s_to_c(path);
	if (!senderok(rcpt)) {
		rejectcount++;
		syslog(0, "smtpd", "Disallowed sending IP of %s (%s/%s) to %s",
			sender, him, nci->rsys, rcpt);
		reply("550 5.7.1 %s ... sending system not allowed\r\n", rcpt);
		return;
	}

	logged = 0;

	/* forwarding() can modify 'path' on loopback request */
	if(filterstate == ACCEPT && fflag && !authenticated && forwarding(path)) {
		syslog(0, "smtpd", "Bad Forward %s (%s/%s) (%s)",
			s_to_c(senders.last->p), him, nci->rsys, s_to_c(path));
		rejectcount++;
		reply("550 5.7.1 we don't relay.  send to your-path@[] for "
			"loopback.\r\n");
		return;
	}
	listadd(&rcvers, path);
	reply("250 2.0.0 receiver is %s\r\n", s_to_c(path));
}

void
quit(void)
{
	reply("221 2.0.0 Successful termination\r\n");
	close(0);
	exits(0);
}

void
noop(void)
{
	if(rejectcheck())
		return;
	reply("250 2.0.0 Nothing to see here. Move along ...\r\n");
}

void
help(String *cmd)
{
	if(rejectcheck())
		return;
	if(cmd)
		s_free(cmd);
	reply("250 2.0.0 See http://www.ietf.org/rfc/rfc2821\r\n");
}

void
verify(String *path)
{
	char *p, *q;
	char *av[4];

	if(rejectcheck())
		return;
	if(shellchars(s_to_c(path))){
		reply("503 5.1.3 Bad character in address %s.\r\n", s_to_c(path));
		return;
	}
	av[0] = s_to_c(mailer);
	av[1] = "-x";
	av[2] = s_to_c(path);
	av[3] = 0;

	pp = noshell_proc_start(av, (stream *)0, outstream(),  (stream *)0, 1, 0);
	if (pp == 0) {
		reply("450 4.3.2 We're busy right now, try later\r\n");
		return;
	}

	p = Brdline(pp->std[1]->fp, '\n');
	if(p == 0){
		reply("550 5.1.0 String does not match anything.\r\n");
	} else {
		p[Blinelen(pp->std[1]->fp)-1] = 0;
		if(strchr(p, ':'))
			reply("550 5.1.0  String does not match anything.\r\n");
		else{
			q = strrchr(p, '!');
			if(q)
				p = q+1;
			reply("250 2.0.0 %s <%s@%s>\r\n", s_to_c(path), p, dom);
		}
	}
	proc_wait(pp);
	proc_free(pp);
	pp = 0;
}

/*
 *  get a line that ends in crnl or cr, turn terminating crnl into a nl
 *
 *  return 0 on EOF
 */
static int
getcrnl(String *s, Biobuf *fp)
{
	int c;

	for(;;){
		c = Bgetc(fp);
		if(debug) {
			seek(2, 0, 2);
			fprint(2, "%c", c);
		}
		switch(c){
		case -1:
			goto out;
		case '\r':
			c = Bgetc(fp);
			if(c == '\n'){
				if(debug) {
					seek(2, 0, 2);
					fprint(2, "%c", c);
				}
				s_putc(s, '\n');
				goto out;
			}
			Bungetc(fp);
			s_putc(s, '\r');
			break;
		case '\n':
			s_putc(s, c);
			goto out;
		default:
			s_putc(s, c);
			break;
		}
	}
out:
	s_terminate(s);
	return s_len(s);
}

void
logcall(int nbytes)
{
	Link *l;
	String *to, *from;

	to = s_new();
	from = s_new();
	for(l = senders.first; l; l = l->next){
		if(l != senders.first)
			s_append(from, ", ");
		s_append(from, s_to_c(l->p));
	}
	for(l = rcvers.first; l; l = l->next){
		if(l != rcvers.first)
			s_append(to, ", ");
		s_append(to, s_to_c(l->p));
	}
	syslog(0, "smtpd", "[%s/%s] %s sent %d bytes to %s", him, nci->rsys,
		s_to_c(from), nbytes, s_to_c(to));
	s_free(to);
	s_free(from);
}

static void
logmsg(char *action)
{
	Link *l;

	if(logged)
		return;

	logged = 1;
	for(l = rcvers.first; l; l = l->next)
		syslog(0, "smtpd", "%s %s (%s/%s) (%s)", action,
			s_to_c(senders.last->p), him, nci->rsys, s_to_c(l->p));
}

static int
optoutall(int filterstate)
{
	Link *l;

	switch(filterstate){
	case ACCEPT:
	case TRUSTED:
		return filterstate;
	}

	for(l = rcvers.first; l; l = l->next)
		if(!optoutofspamfilter(s_to_c(l->p)))
			return filterstate;

	return ACCEPT;
}

String*
startcmd(void)
{
	int n;
	char *filename;
	char **av;
	Link *l;
	String *cmd;

	/*
	 *  ignore the filterstate if the all the receivers prefer it.
	 */
	filterstate = optoutall(filterstate);

	switch (filterstate){
	case BLOCKED:
	case DELAY:
		rejectcount++;
		logmsg("Blocked");
		filename = dumpfile(s_to_c(senders.last->p));
		cmd = s_new();
		s_append(cmd, "cat > ");
		s_append(cmd, filename);
		pp = proc_start(s_to_c(cmd), instream(), 0, outstream(), 0, 0);
		break;
	case DIALUP:
		logmsg("Dialup");
		rejectcount++;
		reply("554 5.7.1 We don't accept mail from dial-up ports.\r\n");
		/*
		 * we could exit here, because we're never going to accept mail
		 * from this ip address, but it's unclear that RFC821 allows
		 * that.  Instead we set the hardreject flag and go stupid.
		 */
		hardreject = 1;
		return 0;
	case DENIED:
		logmsg("Denied");
		rejectcount++;
		reply("554-5.7.1 We don't accept mail from %s.\r\n",
			s_to_c(senders.last->p));
		reply("554 5.7.1 Contact postmaster@%s for more information.\r\n",
			dom);
		return 0;
	case REFUSED:
		logmsg("Refused");
		rejectcount++;
		reply("554 5.7.1 Sender domain must exist: %s\r\n",
			s_to_c(senders.last->p));
		return 0;
	default:
	case NONE:
		logmsg("Confused");
		rejectcount++;
		reply("554-5.7.0 We have had an internal mailer error "
			"classifying your message.\r\n");
		reply("554-5.7.0 Filterstate is %d\r\n", filterstate);
		reply("554 5.7.0 Contact postmaster@%s for more information.\r\n",
			dom);
		return 0;
	case ACCEPT:
	case TRUSTED:
		/*
		 * now that all other filters have been passed,
		 * do grey-list processing.
		 */
		if(gflag)
			vfysenderhostok();

		/*
		 *  set up mail command
		 */
		cmd = s_clone(mailer);
		n = 3;
		for(l = rcvers.first; l; l = l->next)
			n++;
		av = malloc(n * sizeof(char*));
		if(av == nil){
			reply("450 4.3.2 We're busy right now, try later\r\n");
			s_free(cmd);
			return 0;
		}

		n = 0;
		av[n++] = s_to_c(cmd);
		av[n++] = "-r";
		for(l = rcvers.first; l; l = l->next)
			av[n++] = s_to_c(l->p);
		av[n] = 0;
		/*
		 *  start mail process
		 */
		pp = noshell_proc_start(av, instream(), outstream(),
			outstream(), 0, 0);
		free(av);
		break;
	}
	if(pp == 0) {
		reply("450 4.3.2 We're busy right now, try later\r\n");
		s_free(cmd);
		return 0;
	}
	return cmd;
}

/*
 *  print out a header line, expanding any domainless addresses into
 *  address@him
 */
char*
bprintnode(Biobuf *b, Node *p)
{
	if(p->s){
		if(p->addr && strchr(s_to_c(p->s), '@') == nil){
			if(Bprint(b, "%s@%s", s_to_c(p->s), him) < 0)
				return nil;
		} else {
			if(Bwrite(b, s_to_c(p->s), s_len(p->s)) < 0)
				return nil;
		}
	}else{
		if(Bputc(b, p->c) < 0)
			return nil;
	}
	if(p->white)
		if(Bwrite(b, s_to_c(p->white), s_len(p->white)) < 0)
			return nil;
	return p->end+1;
}

static String*
getaddr(Node *p)
{
	for(; p; p = p->next)
		if(p->s && p->addr)
			return p->s;
	return nil;
}

/*
 *  add warning headers of the form
 *	X-warning: <reason>
 *  for any headers that looked like they might be forged.
 *
 *  return byte count of new headers
 */
static int
forgedheaderwarnings(void)
{
	int nbytes;
	Field *f;

	nbytes = 0;

	/* warn about envelope sender */
	if(strcmp(s_to_c(senders.last->p), "/dev/null") != 0 &&
	    masquerade(senders.last->p, nil))
		nbytes += Bprint(pp->std[0]->fp,
			"X-warning: suspect envelope domain\n");

	/*
	 *  check Sender: field.  If it's OK, ignore the others because this
	 *  is an exploded mailing list.
	 */
	for(f = firstfield; f; f = f->next)
		if(f->node->c == SENDER)
			if(masquerade(getaddr(f->node), him))
				nbytes += Bprint(pp->std[0]->fp,
					"X-warning: suspect Sender: domain\n");
			else
				return nbytes;

	/* check From: */
	for(f = firstfield; f; f = f->next){
		if(f->node->c == FROM && masquerade(getaddr(f->node), him))
			nbytes += Bprint(pp->std[0]->fp,
				"X-warning: suspect From: domain\n");
	}
	return nbytes;
}

/*
 *  pipe message to mailer with the following transformations:
 *	- change \r\n into \n.
 *	- add sender's domain to any addrs with no domain
 *	- add a From: if none of From:, Sender:, or Replyto: exists
 *	- add a Received: line
 */
int
pipemsg(int *byteswritten)
{
	int n, nbytes, sawdot, status;
	char *cp;
	Field *f;
	Link *l;
	Node *p;
	String *hdr, *line;

	pipesig(&status);	/* set status to 1 on write to closed pipe */
	sawdot = 0;
	status = 0;

	/*
	 *  add a 'From ' line as envelope
	 */
	nbytes = 0;
	nbytes += Bprint(pp->std[0]->fp, "From %s %s remote from \n",
		s_to_c(senders.first->p), thedate());

	/*
	 *  add our own Received: stamp
	 */
	nbytes += Bprint(pp->std[0]->fp, "Received: from %s ", him);
	if(nci->rsys)
		nbytes += Bprint(pp->std[0]->fp, "([%s]) ", nci->rsys);
	nbytes += Bprint(pp->std[0]->fp, "by %s; %s\n", me, thedate());

	/*
	 *  read first 16k obeying '.' escape.  we're assuming
	 *  the header will all be there.
	 */
	line = s_new();
	hdr = s_new();
	while(sawdot == 0 && s_len(hdr) < 16*1024){
		n = getcrnl(s_reset(line), &bin);

		/* eof or error ends the message */
		if(n <= 0)
			break;

		/* a line with only a '.' ends the message */
		cp = s_to_c(line);
		if(n == 2 && *cp == '.' && *(cp+1) == '\n'){
			sawdot = 1;
			break;
		}

		s_append(hdr, *cp == '.' ? cp+1 : cp);
	}

	/*
	 *  parse header
	 */
	yyinit(s_to_c(hdr), s_len(hdr));
	yyparse();

	/*
	 *  Look for masquerades.  Let Sender: trump From: to allow mailing list
	 *  forwarded messages.
	 */
	if(fflag)
		nbytes += forgedheaderwarnings();

	/*
	 *  add an orginator and/or destination if either is missing
	 */
	if(originator == 0){
		if(senders.last == nil)
			Bprint(pp->std[0]->fp, "From: /dev/null@%s\n", him);
		else
			Bprint(pp->std[0]->fp, "From: %s\n",
				s_to_c(senders.last->p));
	}
	if(destination == 0){
		Bprint(pp->std[0]->fp, "To: ");
		for(l = rcvers.first; l; l = l->next){
			if(l != rcvers.first)
				Bprint(pp->std[0]->fp, ", ");
			Bprint(pp->std[0]->fp, "%s", s_to_c(l->p));
		}
		Bprint(pp->std[0]->fp, "\n");
	}

	/*
	 *  add sender's domain to any domainless addresses
	 *  (to avoid forging local addresses)
	 */
	cp = s_to_c(hdr);
	for(f = firstfield; cp != nil && f; f = f->next){
		for(p = f->node; cp != 0 && p; p = p->next)
			cp = bprintnode(pp->std[0]->fp, p);
		if(status == 0 && Bprint(pp->std[0]->fp, "\n") < 0){
			piperror = "write error";
			status = 1;
		}
	}
	if(cp == nil){
		piperror = "sender domain";
		status = 1;
	}

	/* write anything we read following the header */
	if(status == 0 &&
	    Bwrite(pp->std[0]->fp, cp, s_to_c(hdr) + s_len(hdr) - cp) < 0){
		piperror = "write error 2";
		status = 1;
	}
	s_free(hdr);

	/*
	 *  pass rest of message to mailer.  take care of '.'
	 *  escapes.
	 */
	while(sawdot == 0){
		n = getcrnl(s_reset(line), &bin);

		/* eof or error ends the message */
		if(n <= 0)
			break;

		/* a line with only a '.' ends the message */
		cp = s_to_c(line);
		if(n == 2 && *cp == '.' && *(cp+1) == '\n'){
			sawdot = 1;
			break;
		}
		nbytes += n;
		if(status == 0 && Bwrite(pp->std[0]->fp, *cp == '.' ? cp+1 : cp, n) < 0){
			piperror = "write error 3";
			status = 1;
		}
	}
	s_free(line);
	if(sawdot == 0){
		/* message did not terminate normally */
		snprint(pipbuf, sizeof pipbuf, "network eof: %r");
		piperror = pipbuf;
		syskillpg(pp->pid);
		status = 1;
	}

	if(status == 0 && Bflush(pp->std[0]->fp) < 0){
		piperror = "write error 4";
		status = 1;
	}
	stream_free(pp->std[0]);
	pp->std[0] = 0;
	*byteswritten = nbytes;
	pipesigoff();
	if(status && !piperror)
		piperror = "write on closed pipe";
	return status;
}

char*
firstline(char *x)
{
	char *p;
	static char buf[128];

	strncpy(buf, x, sizeof(buf));
	buf[sizeof(buf)-1] = 0;
	p = strchr(buf, '\n');
	if(p)
		*p = 0;
	return buf;
}

int
sendermxcheck(void)
{
	int pid;
	char *cp, *senddom, *user, *who;
	Waitmsg *w;

	senddom = 0;
	who = s_to_c(senders.first->p);
	if(strcmp(who, "/dev/null") == 0){
		/* /dev/null can only send to one rcpt at a time */
		if(rcvers.first != rcvers.last){
			werrstr("rejected: /dev/null sending to multiple "
				"recipients");
			return -1;
		}
		/* 4408 spf §2.2 notes that 2821 says /dev/null == postmaster@domain */
		senddom = smprint("%s!postmaster", him);
	}

	if(access("/mail/lib/validatesender", AEXEC) < 0)
		return 0;
	if(!senddom)
		senddom = strdup(who);
	if((cp = strchr(senddom, '!')) == nil){
		werrstr("rejected: domainless sender %s", who);
		free(senddom);
		return -1;
	}
	*cp++ = 0;
	user = cp;
	if(shellchars(senddom) || shellchars(user) || shellchars(him)){
		werrstr("rejected: evil sender/domain/helo");
		free(senddom);
		return -1;
	}

	switch(pid = fork()){
	case -1:
		werrstr("deferred: fork: %r");
		return -1;
	case 0:
		/*
		 * Could add an option with the remote IP address
		 * to allow validatesender to implement SPF eventually.
		 */
		execl("/mail/lib/validatesender", "validatesender",
			"-n", nci->root, senddom, user, nci->rsys, him, nil);
		_exits("exec validatesender: %r");
	default:
		break;
	}

	free(senddom);
	w = wait();
	if(w == nil){
		werrstr("deferred: wait failed: %r");
		return -1;
	}
	if(w->pid != pid){
		werrstr("deferred: wait returned wrong pid %d != %d",
			w->pid, pid);
		free(w);
		return -1;
	}
	if(w->msg[0] == 0){
		free(w);
		return 0;
	}
	/*
	 * skip over validatesender 143123132: prefix from rc.
	 */
	cp = strchr(w->msg, ':');
	if(cp && *(cp+1) == ' ')
		werrstr("%s", cp+2);
	else
		werrstr("%s", w->msg);
	free(w);
	return -1;
}

void
data(void)
{
	int status, nbytes;
	char *cp, *ep;
	char errx[ERRMAX];
	Link *l;
	String *cmd, *err;

	if(rejectcheck())
		return;
	if(senders.last == 0){
		reply("503 2.5.2 Data without MAIL FROM:\r\n");
		rejectcount++;
		return;
	}
	if(rcvers.last == 0){
		reply("503 2.5.2 Data without RCPT TO:\r\n");
		rejectcount++;
		return;
	}
	if(!trusted && sendermxcheck()){
		rerrstr(errx, sizeof errx);
		if(strncmp(errx, "rejected:", 9) == 0)
			reply("554 5.7.1 %s\r\n", errx);
		else
			reply("450 4.7.0 %s\r\n", errx);
		for(l=rcvers.first; l; l=l->next)
			syslog(0, "smtpd", "[%s/%s] %s -> %s sendercheck: %s",
				him, nci->rsys, s_to_c(senders.first->p),
				s_to_c(l->p), errx);
		rejectcount++;
		return;
	}

	cmd = startcmd();
	if(cmd == 0)
		return;

	reply("354 Input message; end with <CRLF>.<CRLF>\r\n");

	/*
	 *  allow 145 more minutes to move the data
	 */
	alarm(145*60*1000);

	status = pipemsg(&nbytes);

	/*
	 *  read any error messages
	 */
	err = s_new();
	while(s_read_line(pp->std[2]->fp, err))
		;

	alarm(0);
	atnotify(catchalarm, 0);

	status |= proc_wait(pp);
	if(debug){
		seek(2, 0, 2);
		fprint(2, "%d status %ux\n", getpid(), status);
		if(*s_to_c(err))
			fprint(2, "%d error %s\n", getpid(), s_to_c(err));
	}

	/*
	 *  if process terminated abnormally, send back error message
	 */
	if(status){
		int code;
		char *ecode;

		if(strstr(s_to_c(err), "mail refused")){
			syslog(0, "smtpd", "++[%s/%s] %s %s refused: %s",
				him, nci->rsys, s_to_c(senders.first->p),
				s_to_c(cmd), firstline(s_to_c(err)));
			code = 554;
			ecode = "5.0.0";
		} else {
			syslog(0, "smtpd", "++[%s/%s] %s %s %s%s%sreturned %#q %s",
				him, nci->rsys,
				s_to_c(senders.first->p), s_to_c(cmd),
				piperror? "error during pipemsg: ": "",
				piperror? piperror: "",
				piperror? "; ": "",
				pp->waitmsg->msg, firstline(s_to_c(err)));
			code = 450;
			ecode = "4.0.0";
		}
		for(cp = s_to_c(err); ep = strchr(cp, '\n'); cp = ep){
			*ep++ = 0;
			reply("%d-%s %s\r\n", code, ecode, cp);
		}
		reply("%d %s mail process terminated abnormally\r\n",
			code, ecode);
	} else {
		/*
		 * if a message appeared on stderr, despite good status,
		 * log it.  this can happen if rewrite.in contains a bad
		 * r.e., for example.
		 */
		if(*s_to_c(err))
			syslog(0, "smtpd",
				"%s returned good status, but said: %s",
				s_to_c(mailer), s_to_c(err));

		if(filterstate == BLOCKED)
			reply("554 5.7.1 we believe this is spam.  "
				"we don't accept it.\r\n");
		else if(filterstate == DELAY)
			reply("450 4.3.0 There will be a delay in delivery "
				"of this message.\r\n");
		else {
			reply("250 2.5.0 sent\r\n");
			logcall(nbytes);
		}
	}
	proc_free(pp);
	pp = 0;
	s_free(cmd);
	s_free(err);

	listfree(&senders);
	listfree(&rcvers);
}

/*
 * when we have blocked a transaction based on IP address, there is nothing
 * that the sender can do to convince us to take the message.  after the
 * first rejection, some spammers continually RSET and give a new MAIL FROM:
 * filling our logs with rejections.  rejectcheck() limits the retries and
 * swiftly rejects all further commands after the first 500-series message
 * is issued.
 */
int
rejectcheck(void)
{
	if(rejectcount > MAXREJECTS){
		syslog(0, "smtpd", "Rejected (%s/%s)", him, nci->rsys);
		reply("554 5.5.0 too many errors.  transaction failed.\r\n");
		exits("errcount");
	}
	if(rejectcount)
		sleep(1000 * (4<<rejectcount));
	if(hardreject){
		rejectcount++;
		reply("554 5.7.1 We don't accept mail from dial-up ports.\r\n");
	}
	return hardreject;
}

/*
 *  create abs path of the mailer
 */
String*
mailerpath(char *p)
{
	String *s;

	if(p == nil)
		return nil;
	if(*p == '/')
		return s_copy(p);
	s = s_new();
	s_append(s, UPASBIN);
	s_append(s, "/");
	s_append(s, p);
	return s;
}

String *
s_dec64(String *sin)
{
	int lin, lout;
	String *sout;

	lin = s_len(sin);

	/*
	 * if the string is coming from smtpd.y, it will have no nl.
	 * if it is coming from getcrnl below, it will have an nl.
	 */
	if (*(s_to_c(sin)+lin-1) == '\n')
		lin--;
	sout = s_newalloc(lin+1);
	lout = dec64((uchar *)s_to_c(sout), lin, s_to_c(sin), lin);
	if (lout < 0) {
		s_free(sout);
		return nil;
	}
	sout->ptr = sout->base + lout;
	s_terminate(sout);
	return sout;
}

void
starttls(void)
{
	int certlen, fd;
	uchar *cert;
	TLSconn *conn;

	if (tlscert == nil) {
		reply("500 5.5.1 illegal command or bad syntax\r\n");
		return;
	}
	conn = mallocz(sizeof *conn, 1);
	cert = readcert(tlscert, &certlen);
	if (conn == nil || cert == nil) {
		if (conn != nil)
			free(conn);
		reply("454 4.7.5 TLS not available\r\n");
		return;
	}
	reply("220 2.0.0 Go ahead make my day\r\n");
	conn->cert = cert;
	conn->certlen = certlen;
	fd = tlsServer(Bfildes(&bin), conn);
	if (fd < 0) {
		free(cert);
		free(conn);
		syslog(0, "smtpd", "TLS start-up failed with %s", him);

		/* force the client to hang up */
		close(Bfildes(&bin));		/* probably fd 0 */
		close(1);
		exits("tls failed");
	}
	Bterm(&bin);
	Binit(&bin, fd, OREAD);
	if (dup(fd, 1) < 0)
		fprint(2, "dup of %d failed: %r\n", fd);
	passwordinclear = 1;
	syslog(0, "smtpd", "started TLS with %s", him);
}

void
auth(String *mech, String *resp)
{
	char *user, *pass, *scratch = nil;
	AuthInfo *ai = nil;
	Chalstate *chs = nil;
	String *s_resp1_64 = nil, *s_resp2_64 = nil, *s_resp1 = nil;
	String *s_resp2 = nil;

	if (rejectcheck())
		goto bomb_out;

	syslog(0, "smtpd", "auth(%s, %s) from %s", s_to_c(mech),
		"(protected)", him);

	if (authenticated) {
	bad_sequence:
		rejectcount++;
		reply("503 5.5.2 Bad sequence of commands\r\n");
		goto bomb_out;
	}
	if (cistrcmp(s_to_c(mech), "plain") == 0) {
		if (!passwordinclear) {
			rejectcount++;
			reply("538 5.7.1 Encryption required for requested "
				"authentication mechanism\r\n");
			goto bomb_out;
		}
		s_resp1_64 = resp;
		if (s_resp1_64 == nil) {
			reply("334 \r\n");
			s_resp1_64 = s_new();
			if (getcrnl(s_resp1_64, &bin) <= 0)
				goto bad_sequence;
		}
		s_resp1 = s_dec64(s_resp1_64);
		if (s_resp1 == nil) {
			rejectcount++;
			reply("501 5.5.4 Cannot decode base64\r\n");
			goto bomb_out;
		}
		memset(s_to_c(s_resp1_64), 'X', s_len(s_resp1_64));
		user = (s_to_c(s_resp1) + strlen(s_to_c(s_resp1)) + 1);
		pass = user + (strlen(user) + 1);
		ai = auth_userpasswd(user, pass);
		authenticated = ai != nil;
		memset(pass, 'X', strlen(pass));
		goto windup;
	}
	else if (cistrcmp(s_to_c(mech), "login") == 0) {
		if (!passwordinclear) {
			rejectcount++;
			reply("538 5.7.1 Encryption required for requested "
				"authentication mechanism\r\n");
			goto bomb_out;
		}
		if (resp == nil) {
			reply("334 VXNlcm5hbWU6\r\n");
			s_resp1_64 = s_new();
			if (getcrnl(s_resp1_64, &bin) <= 0)
				goto bad_sequence;
		}
		reply("334 UGFzc3dvcmQ6\r\n");
		s_resp2_64 = s_new();
		if (getcrnl(s_resp2_64, &bin) <= 0)
			goto bad_sequence;
		s_resp1 = s_dec64(s_resp1_64);
		s_resp2 = s_dec64(s_resp2_64);
		memset(s_to_c(s_resp2_64), 'X', s_len(s_resp2_64));
		if (s_resp1 == nil || s_resp2 == nil) {
			rejectcount++;
			reply("501 5.5.4 Cannot decode base64\r\n");
			goto bomb_out;
		}
		ai = auth_userpasswd(s_to_c(s_resp1), s_to_c(s_resp2));
		authenticated = ai != nil;
		memset(s_to_c(s_resp2), 'X', s_len(s_resp2));
windup:
		if (authenticated) {
			/* if you authenticated, we trust you despite your IP */
			trusted = 1;
			reply("235 2.0.0 Authentication successful\r\n");
		} else {
			rejectcount++;
			reply("535 5.7.1 Authentication failed\r\n");
			syslog(0, "smtpd", "authentication failed: %r");
		}
		goto bomb_out;
	}
	else if (cistrcmp(s_to_c(mech), "cram-md5") == 0) {
		int chal64n;
		char *resp, *t;

		chs = auth_challenge("proto=cram role=server");
		if (chs == nil) {
			rejectcount++;
			reply("501 5.7.5 Couldn't get CRAM-MD5 challenge\r\n");
			goto bomb_out;
		}
		scratch = malloc(chs->nchal * 2 + 1);
		chal64n = enc64(scratch, chs->nchal * 2, (uchar *)chs->chal,
			chs->nchal);
		scratch[chal64n] = 0;
		reply("334 %s\r\n", scratch);
		s_resp1_64 = s_new();
		if (getcrnl(s_resp1_64, &bin) <= 0)
			goto bad_sequence;
		s_resp1 = s_dec64(s_resp1_64);
		if (s_resp1 == nil) {
			rejectcount++;
			reply("501 5.5.4 Cannot decode base64\r\n");
			goto bomb_out;
		}
		/* should be of form <user><space><response> */
		resp = s_to_c(s_resp1);
		t = strchr(resp, ' ');
		if (t == nil) {
			rejectcount++;
			reply("501 5.5.4 Poorly formed CRAM-MD5 response\r\n");
			goto bomb_out;
		}
		*t++ = 0;
		chs->user = resp;
		chs->resp = t;
		chs->nresp = strlen(t);
		ai = auth_response(chs);
		authenticated = ai != nil;
		goto windup;
	}
	rejectcount++;
	reply("501 5.5.1 Unrecognised authentication type %s\r\n", s_to_c(mech));
bomb_out:
	if (ai)
		auth_freeAI(ai);
	if (chs)
		auth_freechal(chs);
	if (scratch)
		free(scratch);
	if (s_resp1)
		s_free(s_resp1);
	if (s_resp2)
		s_free(s_resp2);
	if (s_resp1_64)
		s_free(s_resp1_64);
	if (s_resp2_64)
		s_free(s_resp2_64);
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to [email protected].