summaryrefslogtreecommitdiff
path: root/sys/src/cmd/upas/smtp
diff options
context:
space:
mode:
authorTaru Karttunen <taruti@taruti.net>2011-03-30 15:46:40 +0300
committerTaru Karttunen <taruti@taruti.net>2011-03-30 15:46:40 +0300
commite5888a1ffdae813d7575f5fb02275c6bb07e5199 (patch)
treed8d51eac403f07814b9e936eed0c9a79195e2450 /sys/src/cmd/upas/smtp
Import sources from 2011-03-30 iso image
Diffstat (limited to 'sys/src/cmd/upas/smtp')
-rwxr-xr-xsys/src/cmd/upas/smtp/greylist.c317
-rwxr-xr-xsys/src/cmd/upas/smtp/mkfile53
-rwxr-xr-xsys/src/cmd/upas/smtp/mxdial.c359
-rwxr-xr-xsys/src/cmd/upas/smtp/rfc822.y779
-rwxr-xr-xsys/src/cmd/upas/smtp/rmtdns.c59
-rwxr-xr-xsys/src/cmd/upas/smtp/smtp.c1119
-rwxr-xr-xsys/src/cmd/upas/smtp/smtp.h67
-rwxr-xr-xsys/src/cmd/upas/smtp/smtpd.c1736
-rwxr-xr-xsys/src/cmd/upas/smtp/smtpd.h69
-rwxr-xr-xsys/src/cmd/upas/smtp/smtpd.y318
-rwxr-xr-xsys/src/cmd/upas/smtp/spam.c594
11 files changed, 5470 insertions, 0 deletions
diff --git a/sys/src/cmd/upas/smtp/greylist.c b/sys/src/cmd/upas/smtp/greylist.c
new file mode 100755
index 000000000..b5cee5a6e
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/greylist.c
@@ -0,0 +1,317 @@
+/*
+ * greylisting is the practice of making unknown callers call twice, with
+ * a pause between them, before accepting their mail and adding them to a
+ * whitelist of known callers.
+ *
+ * There's a bit of a problem with yahoo and other large sources of mail;
+ * they have a vast pool of machines that all run the same queue(s), so a
+ * 451 retry can come from a different IP address for many, many retries,
+ * and it can take ~5 hours for the same IP to call us back. To cope
+ * better with this, we immediately accept mail from any system on the
+ * same class C subnet (IPv4 /24) as anybody on our whitelist, since the
+ * mail-sending machines tend to be clustered within a class C subnet.
+ *
+ * Various other goofballs, notably the IEEE, try to send mail just
+ * before 9 AM, then refuse to try again until after 5 PM. D'oh!
+ */
+#include "common.h"
+#include "smtpd.h"
+#include "smtp.h"
+#include <ctype.h>
+#include <ip.h>
+#include <ndb.h>
+
+enum {
+ Nonspammax = 14*60*60, /* must call back within this time if real */
+ Nonspammin = 5*60, /* must wait this long to retry */
+};
+
+typedef struct {
+ int existed; /* these two are distinct to cope with errors */
+ int created;
+ int noperm;
+ long mtime; /* mod time, iff it already existed */
+} Greysts;
+
+static char whitelist[] = "/mail/grey/whitelist";
+
+/*
+ * matches ip addresses or subnets in whitelist against nci->rsys.
+ * ignores comments and blank lines in /mail/grey/whitelist.
+ */
+static int
+onwhitelist(void)
+{
+ int lnlen;
+ char *line, *parse, *p;
+ char input[128];
+ uchar *mask;
+ uchar mask4[IPaddrlen], addr4[IPaddrlen];
+ uchar rmask[IPaddrlen], addr[IPaddrlen];
+ uchar ipmasked[IPaddrlen], addrmasked[IPaddrlen];
+ Biobuf *wl;
+
+ wl = Bopen(whitelist, OREAD);
+ if (wl == nil)
+ return 1;
+ while ((line = Brdline(wl, '\n')) != nil) {
+ lnlen = Blinelen(wl);
+ line[lnlen-1] = '\0'; /* clobber newline */
+
+ p = strpbrk(line, " \t");
+ if (p)
+ *p = 0;
+ if (line[0] == '#' || line[0] == 0)
+ continue;
+
+ /* default mask is /24 (v4) or /128 (v6) for bare IP */
+ parse = line;
+ if (strchr(line, '/') == nil) {
+ strecpy(input, input + sizeof input - 5, line);
+ if (strchr(line, ':') != nil) /* v6? */
+ strcat(input, "/128");
+ else if (strchr(line, '.') != nil)
+ strcat(input, "/24"); /* was /32 */
+ parse = input;
+ }
+ mask = rmask;
+ if (strchr(line, ':') != nil) { /* v6? */
+ parseip(addr, parse);
+ p = strchr(parse, '/');
+ if (p != nil)
+ parseipmask(mask, p);
+ else
+ mask = IPallbits;
+ } else {
+ v4parsecidr(addr4, mask4, parse);
+ v4tov6(addr, addr4);
+ v4tov6(mask, mask4);
+ }
+ maskip(addr, mask, addrmasked);
+ maskip(rsysip, mask, ipmasked);
+ if (equivip6(ipmasked, addrmasked))
+ break;
+ }
+ Bterm(wl);
+ return line != nil;
+}
+
+static int mkdirs(char *);
+
+/*
+ * if any directories leading up to path don't exist, create them.
+ * modifies but restores path.
+ */
+static int
+mkpdirs(char *path)
+{
+ int rv = 0;
+ char *sl = strrchr(path, '/');
+
+ if (sl != nil) {
+ *sl = '\0';
+ rv = mkdirs(path);
+ *sl = '/';
+ }
+ return rv;
+}
+
+/*
+ * if path or any directories leading up to it don't exist, create them.
+ * modifies but restores path.
+ */
+static int
+mkdirs(char *path)
+{
+ int fd;
+
+ if (access(path, AEXIST) >= 0)
+ return 0;
+
+ /* make presumed-missing intermediate directories */
+ if (mkpdirs(path) < 0)
+ return -1;
+
+ /* make final directory */
+ fd = create(path, OREAD, 0777|DMDIR);
+ if (fd < 0)
+ /*
+ * we may have lost a race; if the directory now exists,
+ * it's okay.
+ */
+ return access(path, AEXIST) < 0? -1: 0;
+ close(fd);
+ return 0;
+}
+
+static long
+getmtime(char *file)
+{
+ int fd;
+ long mtime = -1;
+ Dir *ds;
+
+ fd = open(file, ORDWR);
+ if (fd < 0)
+ return mtime;
+ ds = dirfstat(fd);
+ if (ds != nil) {
+ mtime = ds->mtime;
+ /*
+ * new twist: update file's mtime after reading it,
+ * so each call resets the future time after which
+ * we'll accept calls. thus spammers who keep pounding
+ * us lose, but just pausing for a few minutes and retrying
+ * will succeed.
+ */
+ if (0) {
+ /*
+ * apparently none can't do this wstat
+ * (permission denied);
+ * more undocumented whacky none behaviour.
+ */
+ ds->mtime = time(0);
+ if (dirfwstat(fd, ds) < 0)
+ syslog(0, "smtpd", "dirfwstat %s: %r", file);
+ }
+ free(ds);
+ write(fd, "x", 1);
+ }
+ close(fd);
+ return mtime;
+}
+
+static void
+tryaddgrey(char *file, Greysts *gsp)
+{
+ int fd = create(file, OWRITE|OEXCL, 0666);
+
+ gsp->created = (fd >= 0);
+ if (fd >= 0) {
+ close(fd);
+ gsp->existed = 0; /* just created; couldn't have existed */
+ gsp->mtime = time(0);
+ } else {
+ /*
+ * why couldn't we create file? it must have existed
+ * (or we were denied perm on parent dir.).
+ * if it existed, fill in gsp->mtime; otherwise
+ * make presumed-missing intermediate directories.
+ */
+ gsp->existed = access(file, AEXIST) >= 0;
+ if (gsp->existed)
+ gsp->mtime = getmtime(file);
+ else if (mkpdirs(file) < 0)
+ gsp->noperm = 1;
+ }
+}
+
+static void
+addgreylist(char *file, Greysts *gsp)
+{
+ tryaddgrey(file, gsp);
+ if (!gsp->created && !gsp->existed && !gsp->noperm)
+ /* retry the greylist entry with parent dirs created */
+ tryaddgrey(file, gsp);
+}
+
+static int
+recentcall(Greysts *gsp)
+{
+ long delay = time(0) - gsp->mtime;
+
+ if (!gsp->existed)
+ return 0;
+ /* reject immediate call-back; spammers are doing that now */
+ return delay >= Nonspammin && delay <= Nonspammax;
+}
+
+/*
+ * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
+ * reject this message as "451 temporary failure". if the caller is real,
+ * he'll retry soon, otherwise he's a spammer.
+ * at the first rejection, create a greylist entry for (my-ip, caller-ip,
+ * rcpt, time), where time is the file's mtime. if they call back and there's
+ * already a greylist entry, and it's within the allowed interval,
+ * add their IP to the append-only whitelist.
+ *
+ * greylist files can be removed at will; at worst they'll cause a few
+ * extra retries.
+ */
+
+static int
+isrcptrecent(char *rcpt)
+{
+ char *user;
+ char file[256];
+ Greysts gs;
+ Greysts *gsp = &gs;
+
+ if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
+ strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
+ return 0;
+
+ /* shorten names to fit pre-fossil or pre-9p2000 file servers */
+ user = strrchr(rcpt, '!');
+ if (user == nil)
+ user = rcpt;
+ else
+ user++;
+
+ /* check & try to update the grey list entry */
+ snprint(file, sizeof file, "/mail/grey/tmp/%s/%s/%s",
+ nci->lsys, nci->rsys, user);
+ memset(gsp, 0, sizeof *gsp);
+ addgreylist(file, gsp);
+
+ /* if on greylist already and prior call was recent, add to whitelist */
+ if (gsp->existed && recentcall(gsp)) {
+ syslog(0, "smtpd",
+ "%s/%s was grey; adding IP to white", nci->rsys, rcpt);
+ return 1;
+ } else if (gsp->existed)
+ syslog(0, "smtpd", "call for %s/%s was just minutes ago "
+ "or long ago", nci->rsys, rcpt);
+ else
+ syslog(0, "smtpd", "no call registered for %s/%s; registering",
+ nci->rsys, rcpt);
+ return 0;
+}
+
+void
+vfysenderhostok(void)
+{
+ char *fqdn;
+ int recent = 0;
+ Link *l;
+
+ if (onwhitelist())
+ return;
+
+ for (l = rcvers.first; l; l = l->next)
+ if (isrcptrecent(s_to_c(l->p)))
+ recent = 1;
+
+ /* if on greylist already and prior call was recent, add to whitelist */
+ if (recent) {
+ int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
+
+ if (fd >= 0) {
+ seek(fd, 0, 2); /* paranoia */
+ fqdn = csgetvalue(nci->root, "ip", nci->rsys, "dom",
+ nil);
+ if (fqdn != nil)
+ fprint(fd, "%s %s\n", nci->rsys, fqdn);
+ else
+ fprint(fd, "%s\n", nci->rsys);
+ free(fqdn);
+ close(fd);
+ }
+ } else {
+ syslog(0, "smtpd",
+ "no recent call from %s for a rcpt; rejecting with temporary failure",
+ nci->rsys);
+ reply("451 please try again soon from the same IP.\r\n");
+ exits("no recent call for a rcpt");
+ }
+}
diff --git a/sys/src/cmd/upas/smtp/mkfile b/sys/src/cmd/upas/smtp/mkfile
new file mode 100755
index 000000000..d49f11238
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/mkfile
@@ -0,0 +1,53 @@
+</$objtype/mkfile
+
+TARG = smtpd\
+ smtp\
+
+OFILES=
+
+LIB=../common/libcommon.a$O\
+
+HFILES=../common/common.h\
+ ../common/sys.h\
+ smtpd.h\
+ smtp.h\
+
+BIN=/$objtype/bin/upas
+UPDATE=\
+ greylist.c\
+ mkfile\
+ mxdial.c\
+ rfc822.y\
+ rmtdns.c\
+ smtpd.y\
+ spam.c\
+ $HFILES\
+ ${OFILES:%.$O=%.c}\
+ ${TARG:%=%.c}\
+
+</sys/src/cmd/mkmany
+CFLAGS=$CFLAGS -I../common -D'SPOOL="/mail"'
+
+$O.smtpd: smtpd.tab.$O rmtdns.$O spam.$O rfc822.tab.$O greylist.$O
+$O.smtp: rfc822.tab.$O mxdial.$O
+
+smtpd.$O: smtpd.h
+
+smtp.$O to.$O: smtp.h
+
+smtpd.tab.c: smtpd.y smtpd.h
+ yacc -o xxx smtpd.y
+ sed 's/yy/zz/g' < xxx > $target
+ rm xxx
+
+rfc822.tab.c: rfc822.y smtp.h
+ yacc -d -o $target rfc822.y
+
+clean:V:
+ rm -f *.[$OS] [$OS].$TARG smtpd.tab.c rfc822.tab.c y.tab.? y.debug $TARG
+
+../common/libcommon.a$O:
+ @{
+ cd ../common
+ mk
+ }
diff --git a/sys/src/cmd/upas/smtp/mxdial.c b/sys/src/cmd/upas/smtp/mxdial.c
new file mode 100755
index 000000000..b5af50327
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/mxdial.c
@@ -0,0 +1,359 @@
+#include "common.h"
+#include <ndb.h>
+#include <smtp.h> /* to publish dial_string_parse */
+
+enum
+{
+ Nmx= 16,
+ Maxstring= 256,
+};
+
+typedef struct Mx Mx;
+struct Mx
+{
+ char host[256];
+ char ip[24];
+ int pref;
+};
+
+char *bustedmxs[Maxbustedmx];
+Ndb *db;
+
+static int mxlookup(DS*, char*);
+static int mxlookup1(DS*, char*);
+static int compar(void*, void*);
+static int callmx(DS*, char*, char*);
+static void expand_meta(DS *ds);
+
+static Mx mx[Nmx];
+
+int
+mxdial(char *addr, char *ddomain, char *gdomain)
+{
+ int fd;
+ DS ds;
+ char err[Errlen];
+
+ addr = netmkaddr(addr, 0, "smtp");
+ dial_string_parse(addr, &ds);
+
+ /* try connecting to destination or any of it's mail routers */
+ fd = callmx(&ds, addr, ddomain);
+
+ /* try our mail gateway */
+ rerrstr(err, sizeof(err));
+ if(fd < 0 && gdomain && strstr(err, "can't translate") != 0)
+ fd = dial(netmkaddr(gdomain, 0, "smtp"), 0, 0, 0);
+
+ return fd;
+}
+
+static int
+busted(char *mx)
+{
+ char **bmp;
+
+ for (bmp = bustedmxs; *bmp != nil; bmp++)
+ if (strcmp(mx, *bmp) == 0)
+ return 1;
+ return 0;
+}
+
+static int
+timeout(void*, char *msg)
+{
+ if(strstr(msg, "alarm"))
+ return 1;
+ return 0;
+}
+
+long
+timedwrite(int fd, void *buf, long len, long ms)
+{
+ long n, oalarm;
+
+ atnotify(timeout, 1);
+ oalarm = alarm(ms);
+ n = write(fd, buf, len);
+ alarm(oalarm);
+ atnotify(timeout, 0);
+ return n;
+}
+
+/*
+ * take an address and return all the mx entries for it,
+ * most preferred first
+ */
+static int
+callmx(DS *ds, char *dest, char *domain)
+{
+ int fd, i, nmx;
+ char addr[Maxstring];
+
+ /* get a list of mx entries */
+ nmx = mxlookup(ds, domain);
+ if(nmx < 0){
+ /* dns isn't working, don't just dial */
+ return -1;
+ }
+ if(nmx == 0){
+ if(debug)
+ fprint(2, "mxlookup returns nothing\n");
+ return dial(dest, 0, 0, 0);
+ }
+
+ /* refuse to honor loopback addresses given by dns */
+ for(i = 0; i < nmx; i++)
+ if(strcmp(mx[i].ip, "127.0.0.1") == 0){
+ if(debug)
+ fprint(2, "mxlookup returns loopback\n");
+ werrstr("illegal: domain lists 127.0.0.1 as mail server");
+ return -1;
+ }
+
+ /* sort by preference */
+ if(nmx > 1)
+ qsort(mx, nmx, sizeof(Mx), compar);
+
+ /* dial each one in turn */
+ for(i = 0; i < nmx; i++){
+ if (busted(mx[i].host)) {
+ if (debug)
+ fprint(2, "mxdial skipping busted mx %s\n",
+ mx[i].host);
+ continue;
+ }
+ snprint(addr, sizeof(addr), "%s/%s!%s!%s", ds->netdir, ds->proto,
+ mx[i].host, ds->service);
+ if(debug)
+ fprint(2, "mxdial trying %s\n", addr);
+ atnotify(timeout, 1);
+ alarm(10*1000);
+ fd = dial(addr, 0, 0, 0);
+ alarm(0);
+ atnotify(timeout, 0);
+ if(fd >= 0)
+ return fd;
+ }
+ return -1;
+}
+
+/*
+ * call the dns process and have it try to resolve the mx request
+ *
+ * this routine knows about the firewall and tries inside and outside
+ * dns's seperately.
+ */
+static int
+mxlookup(DS *ds, char *domain)
+{
+ int n;
+
+ /* just in case we find no domain name */
+ strcpy(domain, ds->host);
+
+ if(ds->netdir)
+ n = mxlookup1(ds, domain);
+ else {
+ ds->netdir = "/net";
+ n = mxlookup1(ds, domain);
+ if(n == 0) {
+ ds->netdir = "/net.alt";
+ n = mxlookup1(ds, domain);
+ }
+ }
+
+ return n;
+}
+
+static int
+mxlookup1(DS *ds, char *domain)
+{
+ int i, n, fd, nmx;
+ char buf[1024], dnsname[Maxstring];
+ char *fields[4];
+
+ snprint(dnsname, sizeof dnsname, "%s/dns", ds->netdir);
+
+ fd = open(dnsname, ORDWR);
+ if(fd < 0)
+ return 0;
+
+ nmx = 0;
+ snprint(buf, sizeof buf, "%s mx", ds->host);
+ if(debug)
+ fprint(2, "sending %s '%s'\n", dnsname, buf);
+ /*
+ * don't hang indefinitely in the write to /net/dns.
+ */
+ n = timedwrite(fd, buf, strlen(buf), 60*1000);
+ if(n < 0){
+ rerrstr(buf, sizeof buf);
+ if(debug)
+ fprint(2, "dns: %s\n", buf);
+ if(strstr(buf, "dns failure")){
+ /* if dns fails for the mx lookup, we have to stop */
+ close(fd);
+ return -1;
+ }
+ } else {
+ /*
+ * get any mx entries
+ */
+ seek(fd, 0, 0);
+ while(nmx < Nmx && (n = read(fd, buf, sizeof buf-1)) > 0){
+ buf[n] = 0;
+ if(debug)
+ fprint(2, "dns mx: %s\n", buf);
+ n = getfields(buf, fields, 4, 1, " \t");
+ if(n < 4)
+ continue;
+
+ if(strchr(domain, '.') == 0)
+ strcpy(domain, fields[0]);
+
+ strncpy(mx[nmx].host, fields[3], sizeof(mx[n].host)-1);
+ mx[nmx].pref = atoi(fields[2]);
+ nmx++;
+ }
+ if(debug)
+ fprint(2, "dns mx; got %d entries\n", nmx);
+ }
+
+ /*
+ * no mx record? try name itself.
+ */
+ /*
+ * BUG? If domain has no dots, then we used to look up ds->host
+ * but return domain instead of ds->host in the list. Now we return
+ * ds->host. What will this break?
+ */
+ if(nmx == 0){
+ mx[0].pref = 1;
+ strncpy(mx[0].host, ds->host, sizeof(mx[0].host));
+ nmx++;
+ }
+
+ /*
+ * look up all ip addresses
+ */
+ for(i = 0; i < nmx; i++){
+ seek(fd, 0, 0);
+ snprint(buf, sizeof buf, "%s ip", mx[i].host);
+ mx[i].ip[0] = 0;
+ /*
+ * don't hang indefinitely in the write to /net/dns.
+ */
+ if(timedwrite(fd, buf, strlen(buf), 60*1000) < 0)
+ goto no;
+ seek(fd, 0, 0);
+ if((n = read(fd, buf, sizeof buf-1)) < 0)
+ goto no;
+ buf[n] = 0;
+ if(getfields(buf, fields, 4, 1, " \t") < 3)
+ goto no;
+ strncpy(mx[i].ip, fields[2], sizeof(mx[i].ip)-1);
+ continue;
+
+ no:
+ /* remove mx[i] and go around again */
+ nmx--;
+ mx[i] = mx[nmx];
+ i--;
+ }
+ return nmx;
+}
+
+static int
+compar(void *a, void *b)
+{
+ return ((Mx*)a)->pref - ((Mx*)b)->pref;
+}
+
+/* break up an address to its component parts */
+void
+dial_string_parse(char *str, DS *ds)
+{
+ char *p, *p2;
+
+ strncpy(ds->buf, str, sizeof(ds->buf));
+ ds->buf[sizeof(ds->buf)-1] = 0;
+
+ p = strchr(ds->buf, '!');
+ if(p == 0) {
+ ds->netdir = 0;
+ ds->proto = "net";
+ ds->host = ds->buf;
+ } else {
+ if(*ds->buf != '/'){
+ ds->netdir = 0;
+ ds->proto = ds->buf;
+ } else {
+ for(p2 = p; *p2 != '/'; p2--)
+ ;
+ *p2++ = 0;
+ ds->netdir = ds->buf;
+ ds->proto = p2;
+ }
+ *p = 0;
+ ds->host = p + 1;
+ }
+ ds->service = strchr(ds->host, '!');
+ if(ds->service)
+ *ds->service++ = 0;
+ if(*ds->host == '$')
+ expand_meta(ds);
+}
+
+static void
+expand_meta(DS *ds)
+{
+ char buf[128], cs[128], *net, *p;
+ int fd, n;
+
+ net = ds->netdir;
+ if(!net)
+ net = "/net";
+
+ if(debug)
+ fprint(2, "expanding %s!%s\n", net, ds->host);
+ snprint(cs, sizeof(cs), "%s/cs", net);
+ if((fd = open(cs, ORDWR)) == -1){
+ if(debug)
+ fprint(2, "open %s: %r\n", cs);
+ syslog(0, "smtp", "cannot open %s: %r", cs);
+ return;
+ }
+
+ snprint(buf, sizeof buf, "!ipinfo %s", ds->host+1); // +1 to skip $
+ if(write(fd, buf, strlen(buf)) <= 0){
+ if(debug)
+ fprint(2, "write %s: %r\n", cs);
+ syslog(0, "smtp", "%s to %s - write failed: %r", buf, cs);
+ close(fd);
+ return;
+ }
+
+ seek(fd, 0, 0);
+ if((n = read(fd, ds->expand, sizeof(ds->expand)-1)) < 0){
+ if(debug)
+ fprint(2, "read %s: %r\n", cs);
+ syslog(0, "smtp", "%s - read failed: %r", cs);
+ close(fd);
+ return;
+ }
+ close(fd);
+
+ ds->expand[n] = 0;
+ if((p = strchr(ds->expand, '=')) == nil){
+ if(debug)
+ fprint(2, "response %s: %s\n", cs, ds->expand);
+ syslog(0, "smtp", "%q from %s - bad response: %r", ds->expand, cs);
+ return;
+ }
+ ds->host = p+1;
+
+ /* take only first one returned (quasi-bug) */
+ if((p = strchr(ds->host, ' ')) != nil)
+ *p = 0;
+}
diff --git a/sys/src/cmd/upas/smtp/rfc822.y b/sys/src/cmd/upas/smtp/rfc822.y
new file mode 100755
index 000000000..b50f63eb1
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/rfc822.y
@@ -0,0 +1,779 @@
+%{
+#include "common.h"
+#include "smtp.h"
+#include <ctype.h>
+
+#define YYMAXDEPTH 500 /* was default 150 */
+
+char *yylp; /* next character to be lex'd */
+int yydone; /* tell yylex to give up */
+char *yybuffer; /* first parsed character */
+char *yyend; /* end of buffer to be parsed */
+Node *root;
+Field *firstfield;
+Field *lastfield;
+Node *usender;
+Node *usys;
+Node *udate;
+char *startfield, *endfield;
+int originator;
+int destination;
+int date;
+int received;
+int messageid;
+%}
+
+%term WORD
+%term DATE
+%term RESENT_DATE
+%term RETURN_PATH
+%term FROM
+%term SENDER
+%term REPLY_TO
+%term RESENT_FROM
+%term RESENT_SENDER
+%term RESENT_REPLY_TO
+%term SUBJECT
+%term TO
+%term CC
+%term BCC
+%term RESENT_TO
+%term RESENT_CC
+%term RESENT_BCC
+%term REMOTE
+%term PRECEDENCE
+%term MIMEVERSION
+%term CONTENTTYPE
+%term MESSAGEID
+%term RECEIVED
+%term MAILER
+%term BADTOKEN
+%start msg
+%%
+
+msg : fields
+ | unixfrom '\n' fields
+ ;
+fields : '\n'
+ { yydone = 1; }
+ | field '\n'
+ | field '\n' fields
+ ;
+field : dates
+ { date = 1; }
+ | originator
+ { originator = 1; }
+ | destination
+ { destination = 1; }
+ | subject
+ | optional
+ | ignored
+ | received
+ | precedence
+ | error '\n' field
+ ;
+unixfrom : FROM route_addr unix_date_time REMOTE FROM word
+ { freenode($1); freenode($4); freenode($5);
+ usender = $2; udate = $3; usys = $6;
+ }
+ ;
+originator : REPLY_TO ':' address_list
+ { newfield(link3($1, $2, $3), 1); }
+ | RETURN_PATH ':' route_addr
+ { newfield(link3($1, $2, $3), 1); }
+ | FROM ':' mailbox_list
+ { newfield(link3($1, $2, $3), 1); }
+ | SENDER ':' mailbox
+ { newfield(link3($1, $2, $3), 1); }
+ | RESENT_REPLY_TO ':' address_list
+ { newfield(link3($1, $2, $3), 1); }
+ | RESENT_SENDER ':' mailbox
+ { newfield(link3($1, $2, $3), 1); }
+ | RESENT_FROM ':' mailbox
+ { newfield(link3($1, $2, $3), 1); }
+ ;
+dates : DATE ':' date_time
+ { newfield(link3($1, $2, $3), 0); }
+ | RESENT_DATE ':' date_time
+ { newfield(link3($1, $2, $3), 0); }
+ ;
+destination : TO ':'
+ { newfield(link2($1, $2), 0); }
+ | TO ':' address_list
+ { newfield(link3($1, $2, $3), 0); }
+ | RESENT_TO ':'
+ { newfield(link2($1, $2), 0); }
+ | RESENT_TO ':' address_list
+ { newfield(link3($1, $2, $3), 0); }
+ | CC ':'
+ { newfield(link2($1, $2), 0); }
+ | CC ':' address_list
+ { newfield(link3($1, $2, $3), 0); }
+ | RESENT_CC ':'
+ { newfield(link2($1, $2), 0); }
+ | RESENT_CC ':' address_list
+ { newfield(link3($1, $2, $3), 0); }
+ | BCC ':'
+ { newfield(link2($1, $2), 0); }
+ | BCC ':' address_list
+ { newfield(link3($1, $2, $3), 0); }
+ | RESENT_BCC ':'
+ { newfield(link2($1, $2), 0); }
+ | RESENT_BCC ':' address_list
+ { newfield(link3($1, $2, $3), 0); }
+ ;
+subject : SUBJECT ':' things
+ { newfield(link3($1, $2, $3), 0); }
+ | SUBJECT ':'
+ { newfield(link2($1, $2), 0); }
+ ;
+received : RECEIVED ':' things
+ { newfield(link3($1, $2, $3), 0); received++; }
+ | RECEIVED ':'
+ { newfield(link2($1, $2), 0); received++; }
+ ;
+precedence : PRECEDENCE ':' things
+ { newfield(link3($1, $2, $3), 0); }
+ | PRECEDENCE ':'
+ { newfield(link2($1, $2), 0); }
+ ;
+ignored : ignoredhdr ':' things
+ { newfield(link3($1, $2, $3), 0); }
+ | ignoredhdr ':'
+ { newfield(link2($1, $2), 0); }
+ ;
+ignoredhdr : MIMEVERSION | CONTENTTYPE | MESSAGEID { messageid = 1; } | MAILER
+ ;
+optional : fieldwords ':' things
+ { /* hack to allow same lex for field names and the rest */
+ if(badfieldname($1)){
+ freenode($1);
+ freenode($2);
+ freenode($3);
+ return 1;
+ }
+ newfield(link3($1, $2, $3), 0);
+ }
+ | fieldwords ':'
+ { /* hack to allow same lex for field names and the rest */
+ if(badfieldname($1)){
+ freenode($1);
+ freenode($2);
+ return 1;
+ }
+ newfield(link2($1, $2), 0);
+ }
+ ;
+address_list : address
+ | address_list ',' address
+ { $$ = link3($1, $2, $3); }
+ ;
+address : mailbox
+ | group
+ ;
+group : phrase ':' address_list ';'
+ { $$ = link2($1, link3($2, $3, $4)); }
+ | phrase ':' ';'
+ { $$ = link3($1, $2, $3); }
+ ;
+mailbox_list : mailbox
+ | mailbox_list ',' mailbox
+ { $$ = link3($1, $2, $3); }
+ ;
+mailbox : route_addr
+ | phrase brak_addr
+ { $$ = link2($1, $2); }
+ | brak_addr
+ ;
+brak_addr : '<' route_addr '>'
+ { $$ = link3($1, $2, $3); }
+ | '<' '>'
+ { $$ = nobody($2); freenode($1); }
+ ;
+route_addr : route ':' at_addr
+ { $$ = address(concat($1, concat($2, $3))); }
+ | addr_spec
+ ;
+route : '@' domain
+ { $$ = concat($1, $2); }
+ | route ',' '@' domain
+ { $$ = concat($1, concat($2, concat($3, $4))); }
+ ;
+addr_spec : local_part
+ { $$ = address($1); }
+ | at_addr
+ ;
+at_addr : local_part '@' domain
+ { $$ = address(concat($1, concat($2, $3)));}
+ | at_addr '@' domain
+ { $$ = address(concat($1, concat($2, $3)));}
+ ;
+local_part : word
+ ;
+domain : word
+ ;
+phrase : word
+ | phrase word
+ { $$ = link2($1, $2); }
+ ;
+things : thing
+ | things thing
+ { $$ = link2($1, $2); }
+ ;
+thing : word | '<' | '>' | '@' | ':' | ';' | ','
+ ;
+date_time : things
+ ;
+unix_date_time : word word word unix_time word word
+ { $$ = link3($1, $3, link3($2, $6, link2($4, $5))); }
+ ;
+unix_time : word
+ | unix_time ':' word
+ { $$ = link3($1, $2, $3); }
+ ;
+word : WORD | DATE | RESENT_DATE | RETURN_PATH | FROM | SENDER
+ | REPLY_TO | RESENT_FROM | RESENT_SENDER | RESENT_REPLY_TO
+ | TO | CC | BCC | RESENT_TO | RESENT_CC | RESENT_BCC | REMOTE | SUBJECT
+ | PRECEDENCE | MIMEVERSION | CONTENTTYPE | MESSAGEID | RECEIVED | MAILER
+ ;
+fieldwords : fieldword
+ | WORD
+ | fieldwords fieldword
+ { $$ = link2($1, $2); }
+ | fieldwords word
+ { $$ = link2($1, $2); }
+ ;
+fieldword : '<' | '>' | '@' | ';' | ','
+ ;
+%%
+
+/*
+ * Initialize the parsing. Done once for each header field.
+ */
+void
+yyinit(char *p, int len)
+{
+ yybuffer = p;
+ yylp = p;
+ yyend = p + len;
+ firstfield = lastfield = 0;
+ received = 0;
+}
+
+/*
+ * keywords identifying header fields we care about
+ */
+typedef struct Keyword Keyword;
+struct Keyword {
+ char *rep;
+ int val;
+};
+
+/* field names that we need to recognize */
+Keyword key[] = {
+ { "date", DATE },
+ { "resent-date", RESENT_DATE },
+ { "return_path", RETURN_PATH },
+ { "from", FROM },
+ { "sender", SENDER },
+ { "reply-to", REPLY_TO },
+ { "resent-from", RESENT_FROM },
+ { "resent-sender", RESENT_SENDER },
+ { "resent-reply-to", RESENT_REPLY_TO },
+ { "to", TO },
+ { "cc", CC },
+ { "bcc", BCC },
+ { "resent-to", RESENT_TO },
+ { "resent-cc", RESENT_CC },
+ { "resent-bcc", RESENT_BCC },
+ { "remote", REMOTE },
+ { "subject", SUBJECT },
+ { "precedence", PRECEDENCE },
+ { "mime-version", MIMEVERSION },
+ { "content-type", CONTENTTYPE },
+ { "message-id", MESSAGEID },
+ { "received", RECEIVED },
+ { "mailer", MAILER },
+ { "who-the-hell-cares", WORD }
+};
+
+/*
+ * Lexical analysis for an rfc822 header field. Continuation lines
+ * are handled in yywhite() when skipping over white space.
+ *
+ */
+yylex(void)
+{
+ String *t;
+ int quoting;
+ int escaping;
+ char *start;
+ Keyword *kp;
+ int c, d;
+
+/* print("lexing\n"); /**/
+ if(yylp >= yyend)
+ return 0;
+ if(yydone)
+ return 0;
+
+ quoting = escaping = 0;
+ start = yylp;
+ yylval = malloc(sizeof(Node));
+ yylval->white = yylval->s = 0;
+ yylval->next = 0;
+ yylval->addr = 0;
+ yylval->start = yylp;
+ for(t = 0; yylp < yyend; yylp++){
+ c = *yylp & 0xff;
+
+ /* dump nulls, they can't be in header */
+ if(c == 0)
+ continue;
+
+ if(escaping) {
+ escaping = 0;
+ } else if(quoting) {
+ switch(c){
+ case '\\':
+ escaping = 1;
+ break;
+ case '\n':
+ d = (*(yylp+1))&0xff;
+ if(d != ' ' && d != '\t'){
+ quoting = 0;
+ yylp--;
+ continue;
+ }
+ break;
+ case '"':
+ quoting = 0;
+ break;
+ }
+ } else {
+ switch(c){
+ case '\\':
+ escaping = 1;
+ break;
+ case '(':
+ case ' ':
+ case '\t':
+ case '\r':
+ goto out;
+ case '\n':
+ if(yylp == start){
+ yylp++;
+/* print("lex(c %c)\n", c); /**/
+ yylval->end = yylp;
+ return yylval->c = c;
+ }
+ goto out;
+ case '@':
+ case '>':
+ case '<':
+ case ':':
+ case ',':
+ case ';':
+ if(yylp == start){
+ yylp++;
+ yylval->white = yywhite();
+/* print("lex(c %c)\n", c); /**/
+ yylval->end = yylp;
+ return yylval->c = c;
+ }
+ goto out;
+ case '"':
+ quoting = 1;
+ break;
+ default:
+ break;
+ }
+ }
+ if(t == 0)
+ t = s_new();
+ s_putc(t, c);
+ }
+out:
+ yylval->white = yywhite();
+ if(t) {
+ s_terminate(t);
+ } else /* message begins with white-space! */
+ return yylval->c = '\n';
+ yylval->s = t;
+ for(kp = key; kp->val != WORD; kp++)
+ if(cistrcmp(s_to_c(t), kp->rep)==0)
+ break;
+/* print("lex(%d) %s\n", kp->val-WORD, s_to_c(t)); /**/
+ yylval->end = yylp;
+ return yylval->c = kp->val;
+}
+
+void
+yyerror(char *x)
+{
+ USED(x);
+
+ /*fprint(2, "parse err: %s\n", x);/**/
+}
+
+/*
+ * parse white space and comments
+ */
+String *
+yywhite(void)
+{
+ String *w;
+ int clevel;
+ int c;
+ int escaping;
+
+ escaping = clevel = 0;
+ for(w = 0; yylp < yyend; yylp++){
+ c = *yylp & 0xff;
+
+ /* dump nulls, they can't be in header */
+ if(c == 0)
+ continue;
+
+ if(escaping){
+ escaping = 0;
+ } else if(clevel) {
+ switch(c){
+ case '\n':
+ /*
+ * look for multiline fields
+ */
+ if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+ break;
+ else
+ goto out;
+ case '\\':
+ escaping = 1;
+ break;
+ case '(':
+ clevel++;
+ break;
+ case ')':
+ clevel--;
+ break;
+ }
+ } else {
+ switch(c){
+ case '\\':
+ escaping = 1;
+ break;
+ case '(':
+ clevel++;
+ break;
+ case ' ':
+ case '\t':
+ case '\r':
+ break;
+ case '\n':
+ /*
+ * look for multiline fields
+ */
+ if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+ break;
+ else
+ goto out;
+ default:
+ goto out;
+ }
+ }
+ if(w == 0)
+ w = s_new();
+ s_putc(w, c);
+ }
+out:
+ if(w)
+ s_terminate(w);
+ return w;
+}
+
+/*
+ * link two parsed entries together
+ */
+Node*
+link2(Node *p1, Node *p2)
+{
+ Node *p;
+
+ for(p = p1; p->next; p = p->next)
+ ;
+ p->next = p2;
+ return p1;
+}
+
+/*
+ * link three parsed entries together
+ */
+Node*
+link3(Node *p1, Node *p2, Node *p3)
+{
+ Node *p;
+
+ for(p = p2; p->next; p = p->next)
+ ;
+ p->next = p3;
+
+ for(p = p1; p->next; p = p->next)
+ ;
+ p->next = p2;
+
+ return p1;
+}
+
+/*
+ * make a:b, move all white space after both
+ */
+Node*
+colon(Node *p1, Node *p2)
+{
+ if(p1->white){
+ if(p2->white)
+ s_append(p1->white, s_to_c(p2->white));
+ } else {
+ p1->white = p2->white;
+ p2->white = 0;
+ }
+
+ s_append(p1->s, ":");
+ if(p2->s)
+ s_append(p1->s, s_to_c(p2->s));
+
+ if(p1->end < p2->end)
+ p1->end = p2->end;
+ freenode(p2);
+ return p1;
+}
+
+/*
+ * concatenate two fields, move all white space after both
+ */
+Node*
+concat(Node *p1, Node *p2)
+{
+ char buf[2];
+
+ if(p1->white){
+ if(p2->white)
+ s_append(p1->white, s_to_c(p2->white));
+ } else {
+ p1->white = p2->white;
+ p2->white = 0;
+ }
+
+ if(p1->s == nil){
+ buf[0] = p1->c;
+ buf[1] = 0;
+ p1->s = s_new();
+ s_append(p1->s, buf);
+ }
+
+ if(p2->s)
+ s_append(p1->s, s_to_c(p2->s));
+ else {
+ buf[0] = p2->c;
+ buf[1] = 0;
+ s_append(p1->s, buf);
+ }
+
+ if(p1->end < p2->end)
+ p1->end = p2->end;
+ freenode(p2);
+ return p1;
+}
+
+/*
+ * look for disallowed chars in the field name
+ */
+int
+badfieldname(Node *p)
+{
+ for(; p; p = p->next){
+ /* field name can't contain white space */
+ if(p->white && p->next)
+ return 1;
+ }
+ return 0;
+}
+
+/*
+ * mark as an address
+ */
+Node *
+address(Node *p)
+{
+ p->addr = 1;
+ return p;
+}
+
+/*
+ * case independent string compare
+ */
+int
+cistrcmp(char *s1, char *s2)
+{
+ int c1, c2;
+
+ for(; *s1; s1++, s2++){
+ c1 = isupper(*s1) ? tolower(*s1) : *s1;
+ c2 = isupper(*s2) ? tolower(*s2) : *s2;
+ if (c1 != c2)
+ return -1;
+ }
+ return *s2;
+}
+
+/*
+ * free a node
+ */
+void
+freenode(Node *p)
+{
+ Node *tp;
+
+ while(p){
+ tp = p->next;
+ if(p->s)
+ s_free(p->s);
+ if(p->white)
+ s_free(p->white);
+ free(p);
+ p = tp;
+ }
+}
+
+
+/*
+ * an anonymous user
+ */
+Node*
+nobody(Node *p)
+{
+ if(p->s)
+ s_free(p->s);
+ p->s = s_copy("pOsTmAsTeR");
+ p->addr = 1;
+ return p;
+}
+
+/*
+ * add anything that was dropped because of a parse error
+ */
+void
+missing(Node *p)
+{
+ Node *np;
+ char *start, *end;
+ Field *f;
+ String *s;
+
+ start = yybuffer;
+ if(lastfield != nil){
+ for(np = lastfield->node; np; np = np->next)
+ start = np->end+1;
+ }
+
+ end = p->start-1;
+
+ if(end <= start)
+ return;
+
+ if(strncmp(start, "From ", 5) == 0)
+ return;
+
+ np = malloc(sizeof(Node));
+ np->start = start;
+ np->end = end;
+ np->white = nil;
+ s = s_copy("BadHeader: ");
+ np->s = s_nappend(s, start, end-start);
+ np->next = nil;
+
+ f = malloc(sizeof(Field));
+ f->next = 0;
+ f->node = np;
+ f->source = 0;
+ if(firstfield)
+ lastfield->next = f;
+ else
+ firstfield = f;
+ lastfield = f;
+}
+
+/*
+ * create a new field
+ */
+void
+newfield(Node *p, int source)
+{
+ Field *f;
+
+ missing(p);
+
+ f = malloc(sizeof(Field));
+ f->next = 0;
+ f->node = p;
+ f->source = source;
+ if(firstfield)
+ lastfield->next = f;
+ else
+ firstfield = f;
+ lastfield = f;
+ endfield = startfield;
+ startfield = yylp;
+}
+
+/*
+ * fee a list of fields
+ */
+void
+freefield(Field *f)
+{
+ Field *tf;
+
+ while(f){
+ tf = f->next;
+ freenode(f->node);
+ free(f);
+ f = tf;
+ }
+}
+
+/*
+ * add some white space to a node
+ */
+Node*
+whiten(Node *p)
+{
+ Node *tp;
+
+ for(tp = p; tp->next; tp = tp->next)
+ ;
+ if(tp->white == 0)
+ tp->white = s_copy(" ");
+ return p;
+}
+
+void
+yycleanup(void)
+{
+ Field *f, *fnext;
+ Node *np, *next;
+
+ for(f = firstfield; f; f = fnext){
+ for(np = f->node; np; np = next){
+ if(np->s)
+ s_free(np->s);
+ if(np->white)
+ s_free(np->white);
+ next = np->next;
+ free(np);
+ }
+ fnext = f->next;
+ free(f);
+ }
+ firstfield = lastfield = 0;
+}
diff --git a/sys/src/cmd/upas/smtp/rmtdns.c b/sys/src/cmd/upas/smtp/rmtdns.c
new file mode 100755
index 000000000..b74a1c90b
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/rmtdns.c
@@ -0,0 +1,59 @@
+#include "common.h"
+#include <ndb.h>
+
+int
+rmtdns(char *net, char *path)
+{
+ int fd, n, nb, r;
+ char *domain, *cp, buf[1024];
+
+ if(net == 0 || path == 0)
+ return 0;
+
+ domain = strdup(path);
+ cp = strchr(domain, '!');
+ if(cp){
+ *cp = 0;
+ n = cp-domain;
+ } else
+ n = strlen(domain);
+
+ if(*domain == '[' && domain[n-1] == ']'){ /* accept [nnn.nnn.nnn.nnn] */
+ domain[n-1] = 0;
+ r = strcmp(ipattr(domain+1), "ip");
+ domain[n-1] = ']';
+ } else
+ r = strcmp(ipattr(domain), "ip"); /* accept nnn.nnn.nnn.nnn */
+ if(r == 0){
+ free(domain);
+ return 0;
+ }
+
+ snprint(buf, sizeof buf, "%s/dns", net);
+ fd = open(buf, ORDWR); /* look up all others */
+ if(fd < 0){ /* dns screw up - can't check */
+ free(domain);
+ return 0;
+ }
+
+ n = snprint(buf, sizeof buf, "%s all", domain);
+ free(domain);
+ seek(fd, 0, 0);
+ nb = write(fd, buf, n);
+ close(fd);
+ if(nb != n){
+ rerrstr(buf, sizeof buf);
+ if (strcmp(buf, "dns: name does not exist") == 0)
+ return -1;
+ }
+ return 0;
+}
+
+/*
+void
+main(int, char *argv[])
+{
+ print("return = %d\n", rmtdns("/net.alt", argv[1]));
+ exits(0);
+}
+*/
diff --git a/sys/src/cmd/upas/smtp/smtp.c b/sys/src/cmd/upas/smtp/smtp.c
new file mode 100755
index 000000000..c336d3a7d
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtp.c
@@ -0,0 +1,1119 @@
+#include "common.h"
+#include "smtp.h"
+#include <ctype.h>
+#include <mp.h>
+#include <libsec.h>
+#include <auth.h>
+
+static char* connect(char*);
+static char* dotls(char*);
+static char* doauth(char*);
+
+void addhostdom(String*, char*);
+String* bangtoat(char*);
+String* convertheader(String*);
+int dBprint(char*, ...);
+int dBputc(int);
+char* data(String*, Biobuf*);
+char* domainify(char*, char*);
+String* fixrouteaddr(String*, Node*, Node*);
+char* getcrnl(String*);
+int getreply(void);
+char* hello(char*, int);
+char* mailfrom(char*);
+int printdate(Node*);
+int printheader(void);
+void putcrnl(char*, int);
+void quit(char*);
+char* rcptto(char*);
+char *rewritezone(char *);
+
+#define Retry "Retry, Temporary Failure"
+#define Giveup "Permanent Failure"
+
+String *reply; /* last reply */
+String *toline;
+
+int alarmscale;
+int autistic;
+int debug; /* true if we're debugging */
+int filter;
+int insecure;
+int last = 'n'; /* last character sent by putcrnl() */
+int ping;
+int quitting; /* when error occurs in quit */
+int tryauth; /* Try to authenticate, if supported */
+int trysecure; /* Try to use TLS if the other side supports it */
+
+char *quitrv; /* deferred return value when in quit */
+char ddomain[1024]; /* domain name of destination machine */
+char *gdomain; /* domain name of gateway */
+char *uneaten; /* first character after rfc822 headers */
+char *farend; /* system we are trying to send to */
+char *user; /* user we are authenticating as, if authenticating */
+char hostdomain[256];
+
+Biobuf bin;
+Biobuf bout;
+Biobuf berr;
+Biobuf bfile;
+
+static int bustedmx;
+
+void
+usage(void)
+{
+ fprint(2, "usage: smtp [-aAdfips] [-b busted-mx] [-g gw] [-h host] "
+ "[-u user] [.domain] net!host[!service] sender rcpt-list\n");
+ exits(Giveup);
+}
+
+int
+timeout(void *x, char *msg)
+{
+ USED(x);
+ syslog(0, "smtp.fail", "interrupt: %s: %s", farend, msg);
+ if(strstr(msg, "alarm")){
+ fprint(2, "smtp timeout: connection to %s timed out\n", farend);
+ if(quitting)
+ exits(quitrv);
+ exits(Retry);
+ }
+ if(strstr(msg, "closed pipe")){
+ /* call _exits() to prevent Bio from trying to flush closed pipe */
+ fprint(2, "smtp timeout: connection closed to %s\n", farend);
+ if(quitting){
+ syslog(0, "smtp.fail", "closed pipe to %s", farend);
+ _exits(quitrv);
+ }
+ _exits(Retry);
+ }
+ return 0;
+}
+
+void
+removenewline(char *p)
+{
+ int n = strlen(p)-1;
+
+ if(n < 0)
+ return;
+ if(p[n] == '\n')
+ p[n] = 0;
+}
+
+void
+main(int argc, char **argv)
+{
+ int i, ok, rcvrs;
+ char *addr, *rv, *trv, *host, *domain;
+ char **errs;
+ char hellodomain[256];
+ String *from, *fromm, *sender;
+
+ alarmscale = 60*1000; /* minutes */
+ quotefmtinstall();
+ errs = malloc(argc*sizeof(char*));
+ reply = s_new();
+ host = 0;
+ ARGBEGIN{
+ case 'a':
+ tryauth = 1;
+ trysecure = 1;
+ break;
+ case 'A': /* autistic: won't talk to us until we talk (Verizon) */
+ autistic = 1;
+ break;
+ case 'b':
+ if (bustedmx >= Maxbustedmx)
+ sysfatal("more than %d busted mxs given", Maxbustedmx);
+ bustedmxs[bustedmx++] = EARGF(usage());
+ break;
+ case 'd':
+ debug = 1;
+ break;
+ case 'f':
+ filter = 1;
+ break;
+ case 'g':
+ gdomain = EARGF(usage());
+ break;
+ case 'h':
+ host = EARGF(usage());
+ break;
+ case 'i':
+ insecure = 1;
+ break;
+ case 'p':
+ alarmscale = 10*1000; /* tens of seconds */
+ ping = 1;
+ break;
+ case 's':
+ trysecure = 1;
+ break;
+ case 'u':
+ user = EARGF(usage());
+ break;
+ default:
+ usage();
+ break;
+ }ARGEND;
+
+ Binit(&berr, 2, OWRITE);
+ Binit(&bfile, 0, OREAD);
+
+ /*
+ * get domain and add to host name
+ */
+ if(*argv && **argv=='.') {
+ domain = *argv;
+ argv++; argc--;
+ } else
+ domain = domainname_read();
+ if(host == 0)
+ host = sysname_read();
+ strcpy(hostdomain, domainify(host, domain));
+ strcpy(hellodomain, domainify(sysname_read(), domain));
+
+ /*
+ * get destination address
+ */
+ if(*argv == 0)
+ usage();
+ addr = *argv++; argc--;
+ farend = addr;
+
+ /*
+ * get sender's machine.
+ * get sender in internet style. domainify if necessary.
+ */
+ if(*argv == 0)
+ usage();
+ sender = unescapespecial(s_copy(*argv++));
+ argc--;
+ fromm = s_clone(sender);
+ rv = strrchr(s_to_c(fromm), '!');
+ if(rv)
+ *rv = 0;
+ else
+ *s_to_c(fromm) = 0;
+ from = bangtoat(s_to_c(sender));
+
+ /*
+ * send the mail
+ */
+ if(filter){
+ Binit(&bout, 1, OWRITE);
+ rv = data(from, &bfile);
+ if(rv != 0)
+ goto error;
+ exits(0);
+ }
+
+ /* mxdial uses its own timeout handler */
+ if((rv = connect(addr)) != 0)
+ exits(rv);
+
+ /* 10 minutes to get through the initial handshake */
+ atnotify(timeout, 1);
+ alarm(10*alarmscale);
+ if((rv = hello(hellodomain, 0)) != 0)
+ goto error;
+ alarm(10*alarmscale);
+ if((rv = mailfrom(s_to_c(from))) != 0)
+ goto error;
+
+ ok = 0;
+ rcvrs = 0;
+ /* if any rcvrs are ok, we try to send the message */
+ for(i = 0; i < argc; i++){
+ if((trv = rcptto(argv[i])) != 0){
+ /* remember worst error */
+ if(rv != Giveup)
+ rv = trv;
+ errs[rcvrs] = strdup(s_to_c(reply));
+ removenewline(errs[rcvrs]);
+ } else {
+ ok++;
+ errs[rcvrs] = 0;
+ }
+ rcvrs++;
+ }
+
+ /* if no ok rcvrs or worst error is retry, give up */
+ if(ok == 0 || rv == Retry)
+ goto error;
+
+ if(ping){
+ quit(0);
+ exits(0);
+ }
+
+ rv = data(from, &bfile);
+ if(rv != 0)
+ goto error;
+ quit(0);
+ if(rcvrs == ok)
+ exits(0);
+
+ /*
+ * here when some but not all rcvrs failed
+ */
+ fprint(2, "%s connect to %s:\n", thedate(), addr);
+ for(i = 0; i < rcvrs; i++){
+ if(errs[i]){
+ syslog(0, "smtp.fail", "delivery to %s at %s failed: %s", argv[i], addr, errs[i]);
+ fprint(2, " mail to %s failed: %s", argv[i], errs[i]);
+ }
+ }
+ exits(Giveup);
+
+ /*
+ * here when all rcvrs failed
+ */
+error:
+ removenewline(s_to_c(reply));
+ syslog(0, "smtp.fail", "%s to %s failed: %s",
+ ping ? "ping" : "delivery",
+ addr, s_to_c(reply));
+ fprint(2, "%s connect to %s:\n%s\n", thedate(), addr, s_to_c(reply));
+ if(!filter)
+ quit(rv);
+ exits(rv);
+}
+
+/*
+ * connect to the remote host
+ */
+static char *
+connect(char* net)
+{
+ char buf[Errlen];
+ int fd;
+
+ fd = mxdial(net, ddomain, gdomain);
+
+ if(fd < 0){
+ rerrstr(buf, sizeof(buf));
+ Bprint(&berr, "smtp: %s (%s)\n", buf, net);
+ syslog(0, "smtp.fail", "%s (%s)", buf, net);
+ if(strstr(buf, "illegal")
+ || strstr(buf, "unknown")
+ || strstr(buf, "can't translate"))
+ return Giveup;
+ else
+ return Retry;
+ }
+ Binit(&bin, fd, OREAD);
+ fd = dup(fd, -1);
+ Binit(&bout, fd, OWRITE);
+ return 0;
+}
+
+static char smtpthumbs[] = "/sys/lib/tls/smtp";
+static char smtpexclthumbs[] = "/sys/lib/tls/smtp.exclude";
+
+/*
+ * exchange names with remote host, attempt to
+ * enable encryption and optionally authenticate.
+ * not fatal if we can't.
+ */
+static char *
+dotls(char *me)
+{
+ TLSconn *c;
+ Thumbprint *goodcerts;
+ char *h;
+ int fd;
+ uchar hash[SHA1dlen];
+
+ c = mallocz(sizeof(*c), 1); /* Note: not freed on success */
+ if (c == nil)
+ return Giveup;
+
+ dBprint("STARTTLS\r\n");
+ if (getreply() != 2)
+ return Giveup;
+
+ fd = tlsClient(Bfildes(&bout), c);
+ if (fd < 0) {
+ syslog(0, "smtp", "tlsClient to %q: %r", ddomain);
+ return Giveup;
+ }
+ goodcerts = initThumbprints(smtpthumbs, smtpexclthumbs);
+ if (goodcerts == nil) {
+ free(c);
+ close(fd);
+ syslog(0, "smtp", "bad thumbprints in %s", smtpthumbs);
+ return Giveup; /* how to recover? TLS is started */
+ }
+
+ /* compute sha1 hash of remote's certificate, see if we know it */
+ sha1(c->cert, c->certlen, hash, nil);
+ if (!okThumbprint(hash, goodcerts)) {
+ /* TODO? if not excluded, add hash to thumb list */
+ free(c);
+ close(fd);
+ h = malloc(2*sizeof hash + 1);
+ if (h != nil) {
+ enc16(h, 2*sizeof hash + 1, hash, sizeof hash);
+ // fprint(2, "x509 sha1=%s", h);
+ syslog(0, "smtp",
+ "remote cert. has bad thumbprint: x509 sha1=%s server=%q",
+ h, ddomain);
+ free(h);
+ }
+ return Giveup; /* how to recover? TLS is started */
+ }
+ freeThumbprints(goodcerts);
+ Bterm(&bin);
+ Bterm(&bout);
+
+ /*
+ * set up bin & bout to use the TLS fd, i/o upon which generates
+ * i/o on the original, underlying fd.
+ */
+ Binit(&bin, fd, OREAD);
+ fd = dup(fd, -1);
+ Binit(&bout, fd, OWRITE);
+
+ syslog(0, "smtp", "started TLS to %q", ddomain);
+ return(hello(me, 1));
+}
+
+static char *
+doauth(char *methods)
+{
+ char *buf, *base64;
+ int n;
+ DS ds;
+ UserPasswd *p;
+
+ dial_string_parse(ddomain, &ds);
+
+ if(user != nil)
+ p = auth_getuserpasswd(nil,
+ "proto=pass service=smtp server=%q user=%q", ds.host, user);
+ else
+ p = auth_getuserpasswd(nil,
+ "proto=pass service=smtp server=%q", ds.host);
+ if (p == nil)
+ return Giveup;
+
+ if (strstr(methods, "LOGIN")){
+ dBprint("AUTH LOGIN\r\n");
+ if (getreply() != 3)
+ return Retry;
+
+ n = strlen(p->user);
+ base64 = malloc(2*n);
+ if (base64 == nil)
+ return Retry; /* Out of memory */
+ enc64(base64, 2*n, (uchar *)p->user, n);
+ dBprint("%s\r\n", base64);
+ if (getreply() != 3)
+ return Retry;
+
+ n = strlen(p->passwd);
+ base64 = malloc(2*n);
+ if (base64 == nil)
+ return Retry; /* Out of memory */
+ enc64(base64, 2*n, (uchar *)p->passwd, n);
+ dBprint("%s\r\n", base64);
+ if (getreply() != 2)
+ return Retry;
+
+ free(base64);
+ }
+ else
+ if (strstr(methods, "PLAIN")){
+ n = strlen(p->user) + strlen(p->passwd) + 3;
+ buf = malloc(n);
+ base64 = malloc(2 * n);
+ if (buf == nil || base64 == nil) {
+ free(buf);
+ return Retry; /* Out of memory */
+ }
+ snprint(buf, n, "%c%s%c%s", 0, p->user, 0, p->passwd);
+ enc64(base64, 2 * n, (uchar *)buf, n - 1);
+ free(buf);
+ dBprint("AUTH PLAIN %s\r\n", base64);
+ free(base64);
+ if (getreply() != 2)
+ return Retry;
+ }
+ else
+ return "No supported AUTH method";
+ return(0);
+}
+
+char *
+hello(char *me, int encrypted)
+{
+ int ehlo;
+ String *r;
+ char *ret, *s, *t;
+
+ if (!encrypted) {
+ /*
+ * Verizon fails to print the smtp greeting banner when it
+ * answers a call. Send a no-op in the hope of making it
+ * talk.
+ */
+ if (autistic) {
+ dBprint("NOOP\r\n");
+ getreply(); /* consume the smtp greeting */
+ /* next reply will be response to noop */
+ }
+ switch(getreply()){
+ case 2:
+ break;
+ case 5:
+ return Giveup;
+ default:
+ return Retry;
+ }
+ }
+
+ ehlo = 1;
+ Again:
+ if(ehlo)
+ dBprint("EHLO %s\r\n", me);
+ else
+ dBprint("HELO %s\r\n", me);
+ switch (getreply()) {
+ case 2:
+ break;
+ case 5:
+ if(ehlo){
+ ehlo = 0;
+ goto Again;
+ }
+ return Giveup;
+ default:
+ return Retry;
+ }
+ r = s_clone(reply);
+ if(r == nil)
+ return Retry; /* Out of memory or couldn't get string */
+
+ /* Invariant: every line has a newline, a result of getcrlf() */
+ for(s = s_to_c(r); (t = strchr(s, '\n')) != nil; s = t + 1){
+ *t = '\0';
+ for (t = s; *t != '\0'; t++)
+ *t = toupper(*t);
+ if(!encrypted && trysecure &&
+ (strcmp(s, "250-STARTTLS") == 0 ||
+ strcmp(s, "250 STARTTLS") == 0)){
+ s_free(r);
+ return dotls(me);
+ }
+ if(tryauth && (encrypted || insecure) &&
+ (strncmp(s, "250 AUTH", strlen("250 AUTH")) == 0 ||
+ strncmp(s, "250-AUTH", strlen("250 AUTH")) == 0)){
+ ret = doauth(s + strlen("250 AUTH "));
+ s_free(r);
+ return ret;
+ }
+ }
+ s_free(r);
+ return 0;
+}
+
+/*
+ * report sender to remote
+ */
+char *
+mailfrom(char *from)
+{
+ if(!returnable(from))
+ dBprint("MAIL FROM:<>\r\n");
+ else
+ if(strchr(from, '@'))
+ dBprint("MAIL FROM:<%s>\r\n", from);
+ else
+ dBprint("MAIL FROM:<%s@%s>\r\n", from, hostdomain);
+ switch(getreply()){
+ case 2:
+ break;
+ case 5:
+ return Giveup;
+ default:
+ return Retry;
+ }
+ return 0;
+}
+
+/*
+ * report a recipient to remote
+ */
+char *
+rcptto(char *to)
+{
+ String *s;
+
+ s = unescapespecial(bangtoat(to));
+ if(toline == 0)
+ toline = s_new();
+ else
+ s_append(toline, ", ");
+ s_append(toline, s_to_c(s));
+ if(strchr(s_to_c(s), '@'))
+ dBprint("RCPT TO:<%s>\r\n", s_to_c(s));
+ else {
+ s_append(toline, "@");
+ s_append(toline, ddomain);
+ dBprint("RCPT TO:<%s@%s>\r\n", s_to_c(s), ddomain);
+ }
+ alarm(10*alarmscale);
+ switch(getreply()){
+ case 2:
+ break;
+ case 5:
+ return Giveup;
+ default:
+ return Retry;
+ }
+ return 0;
+}
+
+static char hex[] = "0123456789abcdef";
+
+/*
+ * send the damn thing
+ */
+char *
+data(String *from, Biobuf *b)
+{
+ char *buf, *cp;
+ int i, n, nbytes, bufsize, eof, r;
+ String *fromline;
+ char errmsg[Errlen];
+ char id[40];
+
+ /*
+ * input the header.
+ */
+
+ buf = malloc(1);
+ if(buf == 0){
+ s_append(s_restart(reply), "out of memory");
+ return Retry;
+ }
+ n = 0;
+ eof = 0;
+ for(;;){
+ cp = Brdline(b, '\n');
+ if(cp == nil){
+ eof = 1;
+ break;
+ }
+ nbytes = Blinelen(b);
+ buf = realloc(buf, n+nbytes+1);
+ if(buf == 0){
+ s_append(s_restart(reply), "out of memory");
+ return Retry;
+ }
+ strncpy(buf+n, cp, nbytes);
+ n += nbytes;
+ if(nbytes == 1) /* end of header */
+ break;
+ }
+ buf[n] = 0;
+ bufsize = n;
+
+ /*
+ * parse the header, turn all addresses into @ format
+ */
+ yyinit(buf, n);
+ yyparse();
+
+ /*
+ * print message observing '.' escapes and using \r\n for \n
+ */
+ alarm(20*alarmscale);
+ if(!filter){
+ dBprint("DATA\r\n");
+ switch(getreply()){
+ case 3:
+ break;
+ case 5:
+ free(buf);
+ return Giveup;
+ default:
+ free(buf);
+ return Retry;
+ }
+ }
+ /*
+ * send header. add a message-id, a sender, and a date if there
+ * isn't one
+ */
+ nbytes = 0;
+ fromline = convertheader(from);
+ uneaten = buf;
+
+ srand(truerand());
+ if(messageid == 0){
+ for(i=0; i<16; i++){
+ r = rand()&0xFF;
+ id[2*i] = hex[r&0xF];
+ id[2*i+1] = hex[(r>>4)&0xF];
+ }
+ id[2*i] = '\0';
+ nbytes += Bprint(&bout, "Message-ID: <%s@%s>\r\n", id, hostdomain);
+ if(debug)
+ Bprint(&berr, "Message-ID: <%s@%s>\r\n", id, hostdomain);
+ }
+
+ if(originator==0){
+ nbytes += Bprint(&bout, "From: %s\r\n", s_to_c(fromline));
+ if(debug)
+ Bprint(&berr, "From: %s\r\n", s_to_c(fromline));
+ }
+ s_free(fromline);
+
+ if(destination == 0 && toline)
+ if(*s_to_c(toline) == '@'){ /* route addr */
+ nbytes += Bprint(&bout, "To: <%s>\r\n", s_to_c(toline));
+ if(debug)
+ Bprint(&berr, "To: <%s>\r\n", s_to_c(toline));
+ } else {
+ nbytes += Bprint(&bout, "To: %s\r\n", s_to_c(toline));
+ if(debug)
+ Bprint(&berr, "To: %s\r\n", s_to_c(toline));
+ }
+
+ if(date==0 && udate)
+ nbytes += printdate(udate);
+ if (usys)
+ uneaten = usys->end + 1;
+ nbytes += printheader();
+ if (*uneaten != '\n')
+ putcrnl("\n", 1);
+
+ /*
+ * send body
+ */
+
+ putcrnl(uneaten, buf+n - uneaten);
+ nbytes += buf+n - uneaten;
+ if(eof == 0){
+ for(;;){
+ n = Bread(b, buf, bufsize);
+ if(n < 0){
+ rerrstr(errmsg, sizeof(errmsg));
+ s_append(s_restart(reply), errmsg);
+ free(buf);
+ return Retry;
+ }
+ if(n == 0)
+ break;
+ alarm(10*alarmscale);
+ putcrnl(buf, n);
+ nbytes += n;
+ }
+ }
+ free(buf);
+ if(!filter){
+ if(last != '\n')
+ dBprint("\r\n.\r\n");
+ else
+ dBprint(".\r\n");
+ alarm(10*alarmscale);
+ switch(getreply()){
+ case 2:
+ break;
+ case 5:
+ return Giveup;
+ default:
+ return Retry;
+ }
+ syslog(0, "smtp", "%s sent %d bytes to %s", s_to_c(from),
+ nbytes, s_to_c(toline));/**/
+ }
+ return 0;
+}
+
+/*
+ * we're leaving
+ */
+void
+quit(char *rv)
+{
+ /* 60 minutes to quit */
+ quitting = 1;
+ quitrv = rv;
+ alarm(60*alarmscale);
+ dBprint("QUIT\r\n");
+ getreply();
+ Bterm(&bout);
+ Bterm(&bfile);
+}
+
+/*
+ * read a reply into a string, return the reply code
+ */
+int
+getreply(void)
+{
+ char *line;
+ int rv;
+
+ reply = s_reset(reply);
+ for(;;){
+ line = getcrnl(reply);
+ if(debug)
+ Bflush(&berr);
+ if(line == 0)
+ return -1;
+ if(!isdigit(line[0]) || !isdigit(line[1]) || !isdigit(line[2]))
+ return -1;
+ if(line[3] != '-')
+ break;
+ }
+ if(debug)
+ Bflush(&berr);
+ rv = atoi(line)/100;
+ return rv;
+}
+void
+addhostdom(String *buf, char *host)
+{
+ s_append(buf, "@");
+ s_append(buf, host);
+}
+
+/*
+ * Convert from `bang' to `source routing' format.
+ *
+ * a.x.y!b.p.o!c!d -> @a.x.y:c!d@b.p.o
+ */
+String *
+bangtoat(char *addr)
+{
+ String *buf;
+ register int i;
+ int j, d;
+ char *field[128];
+
+ /* parse the '!' format address */
+ buf = s_new();
+ for(i = 0; addr; i++){
+ field[i] = addr;
+ addr = strchr(addr, '!');
+ if(addr)
+ *addr++ = 0;
+ }
+ if (i==1) {
+ s_append(buf, field[0]);
+ return buf;
+ }
+
+ /*
+ * count leading domain fields (non-domains don't count)
+ */
+ for(d = 0; d<i-1; d++)
+ if(strchr(field[d], '.')==0)
+ break;
+ /*
+ * if there are more than 1 leading domain elements,
+ * put them in as source routing
+ */
+ if(d > 1){
+ addhostdom(buf, field[0]);
+ for(j=1; j<d-1; j++){
+ s_append(buf, ",");
+ s_append(buf, "@");
+ s_append(buf, field[j]);
+ }
+ s_append(buf, ":");
+ }
+
+ /*
+ * throw in the non-domain elements separated by '!'s
+ */
+ s_append(buf, field[d]);
+ for(j=d+1; j<=i-1; j++) {
+ s_append(buf, "!");
+ s_append(buf, field[j]);
+ }
+ if(d)
+ addhostdom(buf, field[d-1]);
+ return buf;
+}
+
+/*
+ * convert header addresses to @ format.
+ * if the address is a source address, and a domain is specified,
+ * make sure it falls in the domain.
+ */
+String*
+convertheader(String *from)
+{
+ Field *f;
+ Node *p, *lastp;
+ String *a;
+
+ if(!returnable(s_to_c(from))){
+ from = s_new();
+ s_append(from, "Postmaster");
+ addhostdom(from, hostdomain);
+ } else
+ if(strchr(s_to_c(from), '@') == 0){
+ a = username(from);
+ if(a) {
+ s_append(a, " <");
+ s_append(a, s_to_c(from));
+ addhostdom(a, hostdomain);
+ s_append(a, ">");
+ from = a;
+ } else {
+ from = s_copy(s_to_c(from));
+ addhostdom(from, hostdomain);
+ }
+ } else
+ from = s_copy(s_to_c(from));
+ for(f = firstfield; f; f = f->next){
+ lastp = 0;
+ for(p = f->node; p; lastp = p, p = p->next){
+ if(!p->addr)
+ continue;
+ a = bangtoat(s_to_c(p->s));
+ s_free(p->s);
+ if(strchr(s_to_c(a), '@') == 0)
+ addhostdom(a, hostdomain);
+ else if(*s_to_c(a) == '@')
+ a = fixrouteaddr(a, p->next, lastp);
+ p->s = a;
+ }
+ }
+ return from;
+}
+/*
+ * ensure route addr has brackets around it
+ */
+String*
+fixrouteaddr(String *raddr, Node *next, Node *last)
+{
+ String *a;
+
+ if(last && last->c == '<' && next && next->c == '>')
+ return raddr; /* properly formed already */
+
+ a = s_new();
+ s_append(a, "<");
+ s_append(a, s_to_c(raddr));
+ s_append(a, ">");
+ s_free(raddr);
+ return a;
+}
+
+/*
+ * print out the parsed header
+ */
+int
+printheader(void)
+{
+ int n, len;
+ Field *f;
+ Node *p;
+ char *cp;
+ char c[1];
+
+ n = 0;
+ for(f = firstfield; f; f = f->next){
+ for(p = f->node; p; p = p->next){
+ if(p->s)
+ n += dBprint("%s", s_to_c(p->s));
+ else {
+ c[0] = p->c;
+ putcrnl(c, 1);
+ n++;
+ }
+ if(p->white){
+ cp = s_to_c(p->white);
+ len = strlen(cp);
+ putcrnl(cp, len);
+ n += len;
+ }
+ uneaten = p->end;
+ }
+ putcrnl("\n", 1);
+ n++;
+ uneaten++; /* skip newline */
+ }
+ return n;
+}
+
+/*
+ * add a domain onto an name, return the new name
+ */
+char *
+domainify(char *name, char *domain)
+{
+ static String *s;
+ char *p;
+
+ if(domain==0 || strchr(name, '.')!=0)
+ return name;
+
+ s = s_reset(s);
+ s_append(s, name);
+ p = strchr(domain, '.');
+ if(p == 0){
+ s_append(s, ".");
+ p = domain;
+ }
+ s_append(s, p);
+ return s_to_c(s);
+}
+
+/*
+ * print message observing '.' escapes and using \r\n for \n
+ */
+void
+putcrnl(char *cp, int n)
+{
+ int c;
+
+ for(; n; n--, cp++){
+ c = *cp;
+ if(c == '\n')
+ dBputc('\r');
+ else if(c == '.' && last=='\n')
+ dBputc('.');
+ dBputc(c);
+ last = c;
+ }
+}
+
+/*
+ * Get a line including a crnl into a string. Convert crnl into nl.
+ */
+char *
+getcrnl(String *s)
+{
+ int c;
+ int count;
+
+ count = 0;
+ for(;;){
+ c = Bgetc(&bin);
+ if(debug)
+ Bputc(&berr, c);
+ switch(c){
+ case -1:
+ s_append(s, "connection closed unexpectedly by remote system");
+ s_terminate(s);
+ return 0;
+ case '\r':
+ c = Bgetc(&bin);
+ if(c == '\n'){
+ case '\n':
+ s_putc(s, c);
+ if(debug)
+ Bputc(&berr, c);
+ count++;
+ s_terminate(s);
+ return s->ptr - count;
+ }
+ Bungetc(&bin);
+ s_putc(s, '\r');
+ if(debug)
+ Bputc(&berr, '\r');
+ count++;
+ break;
+ default:
+ s_putc(s, c);
+ count++;
+ break;
+ }
+ }
+}
+
+/*
+ * print out a parsed date
+ */
+int
+printdate(Node *p)
+{
+ int n, sep = 0;
+
+ n = dBprint("Date: %s,", s_to_c(p->s));
+ for(p = p->next; p; p = p->next){
+ if(p->s){
+ if(sep == 0) {
+ dBputc(' ');
+ n++;
+ }
+ if (p->next)
+ n += dBprint("%s", s_to_c(p->s));
+ else
+ n += dBprint("%s", rewritezone(s_to_c(p->s)));
+ sep = 0;
+ } else {
+ dBputc(p->c);
+ n++;
+ sep = 1;
+ }
+ }
+ n += dBprint("\r\n");
+ return n;
+}
+
+char *
+rewritezone(char *z)
+{
+ int mindiff;
+ char s;
+ Tm *tm;
+ static char x[7];
+
+ tm = localtime(time(0));
+ mindiff = tm->tzoff/60;
+
+ /* if not in my timezone, don't change anything */
+ if(strcmp(tm->zone, z) != 0)
+ return z;
+
+ if(mindiff < 0){
+ s = '-';
+ mindiff = -mindiff;
+ } else
+ s = '+';
+
+ sprint(x, "%c%.2d%.2d", s, mindiff/60, mindiff%60);
+ return x;
+}
+
+/*
+ * stolen from libc/port/print.c
+ */
+#define SIZE 4096
+int
+dBprint(char *fmt, ...)
+{
+ char buf[SIZE], *out;
+ va_list arg;
+ int n;
+
+ va_start(arg, fmt);
+ out = vseprint(buf, buf+SIZE, fmt, arg);
+ va_end(arg);
+ if(debug){
+ Bwrite(&berr, buf, (long)(out-buf));
+ Bflush(&berr);
+ }
+ n = Bwrite(&bout, buf, (long)(out-buf));
+ Bflush(&bout);
+ return n;
+}
+
+int
+dBputc(int x)
+{
+ if(debug)
+ Bputc(&berr, x);
+ return Bputc(&bout, x);
+}
diff --git a/sys/src/cmd/upas/smtp/smtp.h b/sys/src/cmd/upas/smtp/smtp.h
new file mode 100755
index 000000000..bfac33ed9
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtp.h
@@ -0,0 +1,67 @@
+enum {
+ Maxbustedmx = 100,
+};
+
+typedef struct Node Node;
+typedef struct Field Field;
+typedef Node *Nodeptr;
+#define YYSTYPE Nodeptr
+
+struct Node {
+ Node *next;
+ int c; /* token type */
+ char addr; /* true if this is an address */
+ String *s; /* string representing token */
+ String *white; /* white space following token */
+ char *start; /* first byte for this token */
+ char *end; /* next byte in input */
+};
+
+struct Field {
+ Field *next;
+ Node *node;
+ int source;
+};
+
+typedef struct DS DS;
+struct DS {
+ /* dist string */
+ char buf[128];
+ char expand[128];
+ char *netdir;
+ char *proto;
+ char *host;
+ char *service;
+};
+
+extern Field *firstfield;
+extern Field *lastfield;
+extern Node *usender;
+extern Node *usys;
+extern Node *udate;
+extern int originator;
+extern int destination;
+extern int date;
+extern int debug;
+extern int messageid;
+extern char *bustedmxs[Maxbustedmx];
+
+Node* anonymous(Node*);
+Node* address(Node*);
+int badfieldname(Node*);
+Node* bang(Node*, Node*);
+Node* colon(Node*, Node*);
+int cistrcmp(char*, char*);
+Node* link2(Node*, Node*);
+Node* link3(Node*, Node*, Node*);
+void freenode(Node*);
+void newfield(Node*, int);
+void freefield(Field*);
+void yyinit(char*, int);
+int yyparse(void);
+int yylex(void);
+String* yywhite(void);
+Node* whiten(Node*);
+void yycleanup(void);
+int mxdial(char*, char*, char*);
+void dial_string_parse(char*, DS*);
diff --git a/sys/src/cmd/upas/smtp/smtpd.c b/sys/src/cmd/upas/smtp/smtpd.c
new file mode 100755
index 000000000..8d9ebcb56
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtpd.c
@@ -0,0 +1,1736 @@
+#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;
+
+ulong starttime;
+
+Biobuf bin;
+
+int debug;
+int Dflag;
+int fflag;
+int gflag;
+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 void logmsg(char *action);
+
+static int
+catchalarm(void *a, char *msg)
+{
+ int rv;
+
+ USED(a);
+
+ /* log alarms but continue */
+ if(strstr(msg, "alarm") != nil){
+ if(senders.first && senders.first->p &&
+ rcvers.first && rcvers.first->p)
+ 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 = Atnoterecog;
+ } else
+ rv = Atnoteunknown;
+ if (debug) {
+ seek(2, 0, 2);
+ fprint(2, "caught note: %s\n", msg);
+ }
+
+ /* kill the children if there are any */
+ if(pp && pp->pid > 0) {
+ syskillpg(pp->pid);
+ /* none can't syskillpg, so try a variant */
+ sleep(500);
+ syskill(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);
+ starttime = time(0);
+ 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 '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: %r");
+ parseip(rsysip, nci->rsys);
+
+ if(mailer == nil)
+ mailer = mailerpath("send");
+
+ if(debug){
+ snprint(buf, sizeof buf, "%s/smtpdb/%ld", UPASLOG, time(0));
+ close(2);
+ if (create(buf, OWRITE | OEXCL, 0662) >= 0) {
+ seek(2, 0, 2);
+ fprint(2, "%d smtpd %s\n", getpid(), thedate());
+ } else
+ debug = 0;
+ }
+ getconf();
+ if (isbadguy())
+ exits("banned");
+ 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;
+}
+
+void
+stamp(void)
+{
+ if(debug) {
+ seek(2, 0, 2);
+ fprint(2, "%3lud ", time(0) - starttime);
+ }
+}
+
+#define SIZE 4096
+
+int
+reply(char *fmt, ...)
+{
+ long n;
+ char buf[SIZE], *out;
+ va_list arg;
+
+ va_start(arg, fmt);
+ out = vseprint(buf, buf+SIZE, fmt, arg);
+ va_end(arg);
+
+ n = out - buf;
+ if(debug) {
+ seek(2, 0, 2);
+ stamp();
+ 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);
+}
+
+/*
+ * 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 (trusted)
+ return 0;
+ if (0 && netaspam[rsysip[0]])
+ return 20;
+ return 12;
+}
+
+void
+hello(String *himp, int extended)
+{
+ char **mynames;
+ char *ldot, *rdot;
+
+ him = s_to_c(himp);
+ 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:
+ syslog(0, "smtpd",
+ "Hung up on %s; claimed to be %s",
+ nci->rsys, him);
+ if(Dflag)
+ sleep(delaysecs()*1000);
+ 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, "example") == 0 ||
+ cistrcmp(rdot, "invalid") == 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 && him[0] != '[') {
+ char *p;
+
+ for (p = him; *p != '\0'; p++)
+ if (isascii(*p) && isalpha(*p))
+ break;
+ if (*p == '\0')
+ goto Liarliar;
+ }
+ if(strchr(him, '.') == 0 && nci != nil && strchr(nci->rsys, '.') != nil)
+ him = nci->rsys;
+
+ 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
+ */
+ logged = 0;
+ filterstate = blocked(path);
+ /*
+ * permanently reject what we can before trying smtp ping, which
+ * often leads to merely temporary rejections.
+ */
+ switch (filterstate){
+ case DENIED:
+ syslog(0, "smtpd", "Denied %s (%s/%s)",
+ s_to_c(path), him, nci->rsys);
+ rejectcount++;
+ logged++;
+ reply("554-5.7.1 We don't accept mail from %s.\r\n",
+ s_to_c(path));
+ reply("554 5.7.1 Contact postmaster@%s for more information.\r\n",
+ dom);
+ return;
+ case REFUSED:
+ syslog(0, "smtpd", "Refused %s (%s/%s)",
+ s_to_c(path), him, nci->rsys);
+ rejectcount++;
+ logged++;
+ reply("554 5.7.1 Sender domain must exist: %s\r\n",
+ s_to_c(path));
+ return;
+ }
+
+ 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)",
+ senders.last && senders.last->p?
+ s_to_c(senders.last->p): sender,
+ him, nci->rsys, path? s_to_c(path): rcpt);
+ 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");
+ if(debug){
+ seek(2, 0, 2);
+ stamp();
+ fprint(2, "# %d sent 221 reply to QUIT %s\n",
+ getpid(), thedate());
+ }
+ 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 0:
+ break;
+ case -1:
+ goto out;
+ case '\r':
+ c = Bgetc(fp);
+ if(c == '\n'){
+ if(debug) {
+ seek(2, 0, 2);
+ fprint(2, "%c", c);
+ stamp();
+ }
+ 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:
+ /*
+ * now that all other filters have been passed,
+ * do grey-list processing.
+ */
+ if(gflag)
+ vfysenderhostok();
+ /* fall through */
+
+ case TRUSTED:
+ /*
+ * 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, int *cntp)
+{
+ int len;
+
+ *cntp = 0;
+ 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;
+ *cntp += s_len(p->s) + 1 + strlen(him);
+ } else {
+ len = s_len(p->s);
+ if(Bwrite(b, s_to_c(p->s), len) < 0)
+ return nil;
+ *cntp += len;
+ }
+ }else{
+ if(Bputc(b, p->c) < 0)
+ return nil;
+ ++*cntp;
+ }
+ if(p->white) {
+ len = s_len(p->white);
+ if(Bwrite(b, s_to_c(p->white), len) < 0)
+ return nil;
+ *cntp += len;
+ }
+ 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(senders.last != nil && senders.last->p != nil &&
+ 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, nonhdr, bpr;
+ 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 || senders.last->p == nil)
+ nbytes += Bprint(pp->std[0]->fp, "From: /dev/null@%s\n",
+ him);
+ else
+ nbytes += Bprint(pp->std[0]->fp, "From: %s\n",
+ s_to_c(senders.last->p));
+ }
+ if(destination == 0){
+ nbytes += Bprint(pp->std[0]->fp, "To: ");
+ for(l = rcvers.first; l; l = l->next){
+ if(l != rcvers.first)
+ nbytes += Bprint(pp->std[0]->fp, ", ");
+ nbytes += Bprint(pp->std[0]->fp, "%s", s_to_c(l->p));
+ }
+ nbytes += 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) {
+ bpr = 0;
+ cp = bprintnode(pp->std[0]->fp, p, &bpr);
+ nbytes += bpr;
+ }
+ if(status == 0 && Bprint(pp->std[0]->fp, "\n") < 0){
+ piperror = "write error";
+ status = 1;
+ }
+ nbytes++; /* for newline */
+ }
+ if(cp == nil){
+ piperror = "sender domain";
+ status = 1;
+ }
+
+ /* write anything we read following the header */
+ nonhdr = s_to_c(hdr) + s_len(hdr) - cp;
+ if(status == 0 && Bwrite(pp->std[0]->fp, cp, nonhdr) < 0){
+ piperror = "write error 2";
+ status = 1;
+ }
+ nbytes += nonhdr;
+ 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;
+ }
+ if(cp[0] == '.'){
+ cp++;
+ n--;
+ }
+ nbytes += n;
+ if(status == 0 && Bwrite(pp->std[0]->fp, 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;
+ if (pp->pid > 0) {
+ syskillpg(pp->pid);
+ /* none can't syskillpg, so try a variant */
+ sleep(500);
+ syskill(pp->pid);
+ }
+ status = 1;
+ }
+
+ if(status == 0 && Bflush(pp->std[0]->fp) < 0){
+ piperror = "write error 4";
+ status = 1;
+ }
+ if (debug) {
+ stamp();
+ fprint(2, "at end of message; %s .\n",
+ (sawdot? "saw": "didn't see"));
+ }
+ 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;
+
+ 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;
+ }
+ return 0;
+ }
+
+ if(access("/mail/lib/validatesender", AEXEC) < 0)
+ return 0;
+
+ senddom = strdup(who);
+ if((cp = strchr(senddom, '!')) == nil){
+ werrstr("rejected: domainless sender %s", who);
+ free(senddom);
+ return -1;
+ }
+ *cp++ = 0;
+ user = cp;
+
+ 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, 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");
+ if(debug){
+ seek(2, 0, 2);
+ stamp();
+ fprint(2, "# sent 354; accepting DATA %s\n", thedate());
+ }
+
+
+ /*
+ * allow 145 more minutes to move the data
+ */
+ alarm(145*60*1000);
+
+ status = pipemsg(&nbytes);
+
+ /*
+ * read any error messages
+ */
+ err = s_new();
+ if (debug) {
+ stamp();
+ fprint(2, "waiting for upas/send to close stderr\n");
+ }
+ while(s_read_line(pp->std[2]->fp, err))
+ ;
+
+ alarm(0);
+ atnotify(catchalarm, 0);
+
+ if (debug) {
+ stamp();
+ fprint(2, "waiting for upas/send to exit\n");
+ }
+ status |= proc_wait(pp);
+ if(debug){
+ seek(2, 0, 2);
+ stamp();
+ fprint(2, "# %d upas/send status %#ux at %s\n",
+ getpid(), status, thedate());
+ 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);
+ if(debug){
+ seek(2, 0, 2);
+ stamp();
+ fprint(2, "# %d sent 250 reply %s\n",
+ getpid(), thedate());
+ }
+ }
+ }
+ 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(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);
+}
diff --git a/sys/src/cmd/upas/smtp/smtpd.h b/sys/src/cmd/upas/smtp/smtpd.h
new file mode 100755
index 000000000..be2a1523e
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtpd.h
@@ -0,0 +1,69 @@
+enum {
+ ACCEPT = 0,
+ REFUSED,
+ DENIED,
+ DIALUP,
+ BLOCKED,
+ DELAY,
+ TRUSTED,
+ NONE,
+
+ MAXREJECTS = 100,
+};
+
+
+typedef struct Link Link;
+typedef struct List List;
+
+struct Link {
+ Link *next;
+ String *p;
+};
+
+struct List {
+ Link *first;
+ Link *last;
+};
+
+extern int fflag;
+extern int rflag;
+extern int sflag;
+
+extern int debug;
+extern NetConnInfo *nci;
+extern char *dom;
+extern char* me;
+extern int trusted;
+extern List senders;
+extern List rcvers;
+extern uchar rsysip[];
+
+void addbadguy(char*);
+void auth(String *, String *);
+int blocked(String*);
+void data(void);
+char* dumpfile(char*);
+int forwarding(String*);
+void getconf(void);
+void hello(String*, int extended);
+void help(String *);
+int isbadguy(void);
+void listadd(List*, String*);
+void listfree(List*);
+int masquerade(String*, char*);
+void noop(void);
+int optoutofspamfilter(char*);
+void quit(void);
+void parseinit(void);
+void receiver(String*);
+int recipok(char*);
+int reply(char*, ...);
+void reset(void);
+int rmtdns(char*, char*);
+void sayhi(void);
+void sender(String*);
+void starttls(void);
+void turn(void);
+void verify(String*);
+void vfysenderhostok(void);
+int zzparse(void);
diff --git a/sys/src/cmd/upas/smtp/smtpd.y b/sys/src/cmd/upas/smtp/smtpd.y
new file mode 100755
index 000000000..939b8905d
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtpd.y
@@ -0,0 +1,318 @@
+%{
+#include "common.h"
+#include <ctype.h>
+#include "smtpd.h"
+
+#define YYMAXDEPTH 500 /* was default 150 */
+
+#define YYSTYPE yystype
+typedef struct quux yystype;
+struct quux {
+ String *s;
+ int c;
+};
+Biobuf *yyfp;
+YYSTYPE *bang;
+extern Biobuf bin;
+extern int debug;
+
+YYSTYPE cat(YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*);
+int yyparse(void);
+int yylex(void);
+YYSTYPE anonymous(void);
+%}
+
+%term SPACE
+%term CNTRL
+%term CRLF
+%start conversation
+%%
+
+conversation : cmd
+ | conversation cmd
+ ;
+cmd : error
+ | 'h' 'e' 'l' 'o' spaces sdomain CRLF
+ { hello($6.s, 0); }
+ | 'e' 'h' 'l' 'o' spaces sdomain CRLF
+ { hello($6.s, 1); }
+ | 'm' 'a' 'i' 'l' spaces 'f' 'r' 'o' 'm' ':' spath CRLF
+ { sender($11.s); }
+ | 'm' 'a' 'i' 'l' spaces 'f' 'r' 'o' 'm' ':' spath spaces 'a' 'u' 't' 'h' '=' sauth CRLF
+ { sender($11.s); }
+ | 'r' 'c' 'p' 't' spaces 't' 'o' ':' spath CRLF
+ { receiver($9.s); }
+ | 'd' 'a' 't' 'a' CRLF
+ { data(); }
+ | 'r' 's' 'e' 't' CRLF
+ { reset(); }
+ | 's' 'e' 'n' 'd' spaces 'f' 'r' 'o' 'm' ':' spath CRLF
+ { sender($11.s); }
+ | 's' 'o' 'm' 'l' spaces 'f' 'r' 'o' 'm' ':' spath CRLF
+ { sender($11.s); }
+ | 's' 'a' 'm' 'l' spaces 'f' 'r' 'o' 'm' ':' spath CRLF
+ { sender($11.s); }
+ | 'v' 'r' 'f' 'y' spaces string CRLF
+ { verify($6.s); }
+ | 'e' 'x' 'p' 'n' spaces string CRLF
+ { verify($6.s); }
+ | 'h' 'e' 'l' 'p' CRLF
+ { help(0); }
+ | 'h' 'e' 'l' 'p' spaces string CRLF
+ { help($6.s); }
+ | 'n' 'o' 'o' 'p' CRLF
+ { noop(); }
+ | 'q' 'u' 'i' 't' CRLF
+ { quit(); }
+ | 's' 't' 'a' 'r' 't' 't' 'l' 's' CRLF
+ { starttls(); }
+ | 'a' 'u' 't' 'h' spaces name spaces string CRLF
+ { auth($6.s, $8.s); }
+ | 'a' 'u' 't' 'h' spaces name CRLF
+ { auth($6.s, nil); }
+ | CRLF
+ { reply("500 5.5.1 illegal command or bad syntax\r\n"); }
+ ;
+path : '<' '>' ={ $$ = anonymous(); }
+ | '<' mailbox '>' ={ $$ = $2; }
+ | '<' a_d_l ':' mailbox '>' ={ $$ = cat(&$2, bang, &$4, 0, 0 ,0, 0); }
+ ;
+spath : path ={ $$ = $1; }
+ | spaces path ={ $$ = $2; }
+ ;
+auth : path ={ $$ = $1; }
+ | mailbox ={ $$ = $1; }
+ ;
+sauth : auth ={ $$ = $1; }
+ | spaces auth ={ $$ = $2; }
+ ;
+ ;
+a_d_l : at_domain ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | at_domain ',' a_d_l ={ $$ = cat(&$1, bang, &$3, 0, 0, 0, 0); }
+ ;
+at_domain : '@' domain ={ $$ = cat(&$2, 0, 0, 0, 0 ,0, 0); }
+ ;
+sdomain : domain ={ $$ = $1; }
+ | domain spaces ={ $$ = $1; }
+ ;
+domain : element ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | element '.' ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | element '.' domain ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+ ;
+element : name ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | '#' number ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ | '[' ']' ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ | '[' dotnum ']' ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+ ;
+mailbox : local_part ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | local_part '@' domain ={ $$ = cat(&$3, bang, &$1, 0, 0 ,0, 0); }
+ ;
+local_part : dot_string ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | quoted_string ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ ;
+name : let_dig ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | let_dig ld_str ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ | let_dig ldh_str ld_str ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+ ;
+ld_str : let_dig
+ | let_dig ld_str ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ ;
+ldh_str : hunder
+ | ld_str hunder ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ | ldh_str ld_str hunder ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+ ;
+let_dig : a
+ | d
+ ;
+dot_string : string ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | string '.' dot_string ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+ ;
+
+string : char ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | string char ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ ;
+
+quoted_string : '"' qtext '"' ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+ ;
+qtext : '\\' x ={ $$ = cat(&$2, 0, 0, 0, 0 ,0, 0); }
+ | qtext '\\' x ={ $$ = cat(&$1, &$3, 0, 0, 0 ,0, 0); }
+ | q
+ | qtext q ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ ;
+char : c
+ | '\\' x ={ $$ = $2; }
+ ;
+dotnum : snum '.' snum '.' snum '.' snum ={ $$ = cat(&$1, &$2, &$3, &$4, &$5, &$6, &$7); }
+ ;
+number : d ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+ | number d ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+ ;
+snum : number ={ if(atoi(s_to_c($1.s)) > 255) fprint(2, "bad snum\n"); }
+ ;
+spaces : SPACE ={ $$ = $1; }
+ | SPACE spaces ={ $$ = $1; }
+ ;
+hunder : '-' | '_'
+ ;
+special1 : CNTRL
+ | '(' | ')' | ',' | '.'
+ | ':' | ';' | '<' | '>' | '@'
+ ;
+special : special1 | '\\' | '"'
+ ;
+notspecial : '!' | '#' | '$' | '%' | '&' | '\''
+ | '*' | '+' | '-' | '/'
+ | '=' | '?'
+ | '[' | ']' | '^' | '_' | '`' | '{' | '|' | '}' | '~'
+ ;
+
+a : 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i'
+ | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r'
+ | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
+ ;
+d : '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
+ ;
+c : a | d | notspecial
+ ;
+q : a | d | special1 | notspecial | SPACE
+ ;
+x : a | d | special | notspecial | SPACE
+ ;
+%%
+
+void
+parseinit(void)
+{
+ bang = (YYSTYPE*)malloc(sizeof(YYSTYPE));
+ bang->c = '!';
+ bang->s = 0;
+ yyfp = &bin;
+}
+
+yylex(void)
+{
+ int c;
+
+ for(;;){
+ c = Bgetc(yyfp);
+ if(c == -1)
+ return 0;
+ if(debug)
+ fprint(2, "%c", c);
+ yylval.c = c = c & 0x7F;
+ if(c == '\n'){
+ return CRLF;
+ }
+ if(c == '\r'){
+ c = Bgetc(yyfp);
+ if(c != '\n'){
+ Bungetc(yyfp);
+ c = '\r';
+ } else {
+ if(debug)
+ fprint(2, "%c", c);
+ return CRLF;
+ }
+ }
+ if(isalpha(c))
+ return tolower(c);
+ if(isspace(c))
+ return SPACE;
+ if(iscntrl(c))
+ return CNTRL;
+ return c;
+ }
+}
+
+YYSTYPE
+cat(YYSTYPE *y1, YYSTYPE *y2, YYSTYPE *y3, YYSTYPE *y4, YYSTYPE *y5, YYSTYPE *y6, YYSTYPE *y7)
+{
+ YYSTYPE rv;
+
+ if(y1->s)
+ rv.s = y1->s;
+ else {
+ rv.s = s_new();
+ s_putc(rv.s, y1->c);
+ s_terminate(rv.s);
+ }
+ if(y2){
+ if(y2->s){
+ s_append(rv.s, s_to_c(y2->s));
+ s_free(y2->s);
+ } else {
+ s_putc(rv.s, y2->c);
+ s_terminate(rv.s);
+ }
+ } else
+ return rv;
+ if(y3){
+ if(y3->s){
+ s_append(rv.s, s_to_c(y3->s));
+ s_free(y3->s);
+ } else {
+ s_putc(rv.s, y3->c);
+ s_terminate(rv.s);
+ }
+ } else
+ return rv;
+ if(y4){
+ if(y4->s){
+ s_append(rv.s, s_to_c(y4->s));
+ s_free(y4->s);
+ } else {
+ s_putc(rv.s, y4->c);
+ s_terminate(rv.s);
+ }
+ } else
+ return rv;
+ if(y5){
+ if(y5->s){
+ s_append(rv.s, s_to_c(y5->s));
+ s_free(y5->s);
+ } else {
+ s_putc(rv.s, y5->c);
+ s_terminate(rv.s);
+ }
+ } else
+ return rv;
+ if(y6){
+ if(y6->s){
+ s_append(rv.s, s_to_c(y6->s));
+ s_free(y6->s);
+ } else {
+ s_putc(rv.s, y6->c);
+ s_terminate(rv.s);
+ }
+ } else
+ return rv;
+ if(y7){
+ if(y7->s){
+ s_append(rv.s, s_to_c(y7->s));
+ s_free(y7->s);
+ } else {
+ s_putc(rv.s, y7->c);
+ s_terminate(rv.s);
+ }
+ } else
+ return rv;
+ return rv;
+}
+
+void
+yyerror(char *x)
+{
+ USED(x);
+}
+
+/*
+ * an anonymous user
+ */
+YYSTYPE
+anonymous(void)
+{
+ YYSTYPE rv;
+
+ rv.s = s_copy("/dev/null");
+ return rv;
+}
diff --git a/sys/src/cmd/upas/smtp/spam.c b/sys/src/cmd/upas/smtp/spam.c
new file mode 100755
index 000000000..4b7fe4134
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/spam.c
@@ -0,0 +1,594 @@
+#include "common.h"
+#include "smtpd.h"
+#include <ip.h>
+
+enum {
+ NORELAY = 0,
+ DNSVERIFY,
+ SAVEBLOCK,
+ DOMNAME,
+ OURNETS,
+ OURDOMS,
+
+ IP = 0,
+ STRING,
+};
+
+
+typedef struct Keyword Keyword;
+
+struct Keyword {
+ char *name;
+ int code;
+};
+
+static Keyword options[] = {
+ "norelay", NORELAY,
+ "verifysenderdom", DNSVERIFY,
+ "saveblockedmsg", SAVEBLOCK,
+ "defaultdomain", DOMNAME,
+ "ournets", OURNETS,
+ "ourdomains", OURDOMS,
+ 0, NONE,
+};
+
+static Keyword actions[] = {
+ "allow", ACCEPT,
+ "block", BLOCKED,
+ "deny", DENIED,
+ "dial", DIALUP,
+ "delay", DELAY,
+ 0, NONE,
+};
+
+static int hisaction;
+static List ourdoms;
+static List badguys;
+static ulong v4peerip;
+
+static char* getline(Biobuf*);
+static int cidrcheck(char*);
+
+static int
+findkey(char *val, Keyword *p)
+{
+
+ for(; p->name; p++)
+ if(strcmp(val, p->name) == 0)
+ break;
+ return p->code;
+}
+
+char*
+actstr(int a)
+{
+ static char buf[32];
+ Keyword *p;
+
+ for(p=actions; p->name; p++)
+ if(p->code == a)
+ return p->name;
+ if(a==NONE)
+ return "none";
+ sprint(buf, "%d", a);
+ return buf;
+}
+
+int
+getaction(char *s, char *type)
+{
+ char buf[1024];
+ Keyword *k;
+
+ if(s == nil || *s == 0)
+ return ACCEPT;
+
+ for(k = actions; k->name != 0; k++){
+ snprint(buf, sizeof buf, "/mail/ratify/%s/%s/%s", k->name, type, s);
+ if(access(buf,0) >= 0)
+ return k->code;
+ }
+ return ACCEPT;
+}
+
+int
+istrusted(char *s)
+{
+ char buf[1024];
+
+ if(s == nil || *s == 0)
+ return 0;
+
+ snprint(buf, sizeof buf, "/mail/ratify/trusted/%s", s);
+ return access(buf,0) >= 0;
+}
+
+void
+getconf(void)
+{
+ Biobuf *bp;
+ char *cp, *p;
+ String *s;
+ char buf[512];
+ uchar addr[4];
+
+ v4parseip(addr, nci->rsys);
+ v4peerip = nhgetl(addr);
+
+ trusted = istrusted(nci->rsys);
+ hisaction = getaction(nci->rsys, "ip");
+ if(debug){
+ fprint(2, "istrusted(%s)=%d\n", nci->rsys, trusted);
+ fprint(2, "getaction(%s, ip)=%s\n", nci->rsys, actstr(hisaction));
+ }
+ snprint(buf, sizeof(buf), "%s/smtpd.conf", UPASLIB);
+ bp = sysopen(buf, "r", 0);
+ if(bp == 0)
+ return;
+
+ for(;;){
+ cp = getline(bp);
+ if(cp == 0)
+ break;
+ p = cp+strlen(cp)+1;
+ switch(findkey(cp, options)){
+ case NORELAY:
+ if(fflag == 0 && strcmp(p, "on") == 0)
+ fflag++;
+ break;
+ case DNSVERIFY:
+ if(rflag == 0 && strcmp(p, "on") == 0)
+ rflag++;
+ break;
+ case SAVEBLOCK:
+ if(sflag == 0 && strcmp(p, "on") == 0)
+ sflag++;
+ break;
+ case DOMNAME:
+ if(dom == 0)
+ dom = strdup(p);
+ break;
+ case OURNETS:
+ if (trusted == 0)
+ trusted = cidrcheck(p);
+ break;
+ case OURDOMS:
+ while(*p){
+ s = s_new();
+ s_append(s, p);
+ listadd(&ourdoms, s);
+ p += strlen(p)+1;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ sysclose(bp);
+}
+
+/*
+ * match a user name. the only meta-char is '*' which matches all
+ * characters. we only allow it as "*", which matches anything or
+ * an * at the end of the name (e.g., "username*") which matches
+ * trailing characters.
+ */
+static int
+usermatch(char *pathuser, char *specuser)
+{
+ int n;
+
+ n = strlen(specuser)-1;
+ if(specuser[n] == '*'){
+ if(n == 0) /* match everything */
+ return 0;
+ return strncmp(pathuser, specuser, n);
+ }
+ return strcmp(pathuser, specuser);
+}
+
+static int
+dommatch(char *pathdom, char *specdom)
+{
+ int n;
+
+ if (*specdom == '*'){
+ if (specdom[1] == '.' && specdom[2]){
+ specdom += 2;
+ n = strlen(pathdom)-strlen(specdom);
+ if(n == 0 || (n > 0 && pathdom[n-1] == '.'))
+ return strcmp(pathdom+n, specdom);
+ return n;
+ }
+ }
+ return strcmp(pathdom, specdom);
+}
+
+/*
+ * figure out action for this sender
+ */
+int
+blocked(String *path)
+{
+ String *lpath;
+ int action;
+
+ if(debug)
+ fprint(2, "blocked(%s)\n", s_to_c(path));
+
+ /* if the sender's IP address is blessed, ignore sender email address */
+ if(trusted){
+ if(debug)
+ fprint(2, "\ttrusted => trusted\n");
+ return TRUSTED;
+ }
+
+ /* if sender's IP address is blocked, ignore sender email address */
+ if(hisaction != ACCEPT){
+ if(debug)
+ fprint(2, "\thisaction=%s => %s\n", actstr(hisaction), actstr(hisaction));
+ return hisaction;
+ }
+
+ /* convert to lower case */
+ lpath = s_copy(s_to_c(path));
+ s_tolower(lpath);
+
+ /* classify */
+ action = getaction(s_to_c(lpath), "account");
+ if(debug)
+ fprint(2, "\tgetaction account %s => %s\n", s_to_c(lpath), actstr(action));
+ s_free(lpath);
+ return action;
+}
+
+/*
+ * get a canonicalized line: a string of null-terminated lower-case
+ * tokens with a two null bytes at the end.
+ */
+static char*
+getline(Biobuf *bp)
+{
+ char c, *cp, *p, *q;
+ int n;
+
+ static char *buf;
+ static int bufsize;
+
+ for(;;){
+ cp = Brdline(bp, '\n');
+ if(cp == 0)
+ return 0;
+ n = Blinelen(bp);
+ cp[n-1] = 0;
+ if(buf == 0 || bufsize < n+1){
+ bufsize += 512;
+ if(bufsize < n+1)
+ bufsize = n+1;
+ buf = realloc(buf, bufsize);
+ if(buf == 0)
+ break;
+ }
+ q = buf;
+ for (p = cp; *p; p++){
+ c = *p;
+ if(c == '\\' && p[1]) /* we don't allow \<newline> */
+ c = *++p;
+ else
+ if(c == '#')
+ break;
+ else
+ if(c == ' ' || c == '\t' || c == ',')
+ if(q == buf || q[-1] == 0)
+ continue;
+ else
+ c = 0;
+ *q++ = tolower(c);
+ }
+ if(q != buf){
+ if(q[-1])
+ *q++ = 0;
+ *q = 0;
+ break;
+ }
+ }
+ return buf;
+}
+
+static int
+isourdom(char *s)
+{
+ Link *l;
+
+ if(strchr(s, '.') == nil)
+ return 1;
+
+ for(l = ourdoms.first; l; l = l->next){
+ if(dommatch(s, s_to_c(l->p)) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+int
+forwarding(String *path)
+{
+ char *cp, *s;
+ String *lpath;
+
+ if(debug)
+ fprint(2, "forwarding(%s)\n", s_to_c(path));
+
+ /* first check if they want loopback */
+ lpath = s_copy(s_to_c(s_restart(path)));
+ if(nci->rsys && *nci->rsys){
+ cp = s_to_c(lpath);
+ if(strncmp(cp, "[]!", 3) == 0){
+found:
+ s_append(path, "[");
+ s_append(path, nci->rsys);
+ s_append(path, "]!");
+ s_append(path, cp+3);
+ s_terminate(path);
+ s_free(lpath);
+ return 0;
+ }
+ cp = strchr(cp,'!'); /* skip our domain and check next */
+ if(cp++ && strncmp(cp, "[]!", 3) == 0)
+ goto found;
+ }
+
+ /* if mail is from a trusted IP addr, allow it to forward */
+ if(trusted) {
+ s_free(lpath);
+ return 0;
+ }
+
+ /* sender is untrusted; ensure receiver is in one of our domains */
+ for(cp = s_to_c(lpath); *cp; cp++) /* convert receiver lc */
+ *cp = tolower(*cp);
+
+ for(s = s_to_c(lpath); cp = strchr(s, '!'); s = cp+1){
+ *cp = 0;
+ if(!isourdom(s)){
+ s_free(lpath);
+ return 1;
+ }
+ }
+ s_free(lpath);
+ return 0;
+}
+
+int
+masquerade(String *path, char *him)
+{
+ char *cp, *s;
+ String *lpath;
+ int rv = 0;
+
+ if(debug)
+ fprint(2, "masquerade(%s) ", s_to_c(path));
+
+ if(trusted || path == nil) {
+ if(debug)
+ fprint(2, "0\n");
+ return 0;
+ }
+
+ lpath = s_copy(s_to_c(path));
+
+ /* sender is untrusted; ensure receiver is in one of our domains */
+ for(cp = s_to_c(lpath); *cp; cp++) /* convert receiver lc */
+ *cp = tolower(*cp);
+ s = s_to_c(lpath);
+
+ /* scan first element of ! or last element of @ paths */
+ if((cp = strchr(s, '!')) != nil){
+ *cp = 0;
+ if(isourdom(s))
+ rv = 1;
+ } else if((cp = strrchr(s, '@')) != nil){
+ if(isourdom(cp+1))
+ rv = 1;
+ } else {
+ if(isourdom(him))
+ rv = 1;
+ }
+
+ s_free(lpath);
+ if (debug)
+ fprint(2, "%d\n", rv);
+ return rv;
+}
+
+/* this is a v4 only check */
+static int
+cidrcheck(char *cp)
+{
+ char *p;
+ ulong a, m;
+ uchar addr[IPv4addrlen];
+ uchar mask[IPv4addrlen];
+
+ if(v4peerip == 0)
+ return 0;
+
+ /* parse a list of CIDR addresses comparing each to the peer IP addr */
+ while(cp && *cp){
+ v4parsecidr(addr, mask, cp);
+ a = nhgetl(addr);
+ m = nhgetl(mask);
+ /*
+ * if a mask isn't specified, we build a minimal mask
+ * instead of using the default mask for that net. in this
+ * case we never allow a class A mask (0xff000000).
+ */
+ if(strchr(cp, '/') == 0){
+ m = 0xff000000;
+ p = cp;
+ for(p = strchr(p, '.'); p && p[1]; p = strchr(p+1, '.'))
+ m = (m>>8)|0xff000000;
+
+ /* force at least a class B */
+ m |= 0xffff0000;
+ }
+ if((v4peerip&m) == a)
+ return 1;
+ cp += strlen(cp)+1;
+ }
+ return 0;
+}
+
+int
+isbadguy(void)
+{
+ Link *l;
+
+ /* check if this IP address is banned */
+ for(l = badguys.first; l; l = l->next)
+ if(cidrcheck(s_to_c(l->p)))
+ return 1;
+
+ return 0;
+}
+
+void
+addbadguy(char *p)
+{
+ listadd(&badguys, s_copy(p));
+};
+
+char*
+dumpfile(char *sender)
+{
+ int i, fd;
+ ulong h;
+ static char buf[512];
+ char *cp;
+
+ if (sflag == 1){
+ cp = ctime(time(0));
+ cp[7] = 0;
+ if(cp[8] == ' ')
+ sprint(buf, "%s/queue.dump/%s%c", SPOOL, cp+4, cp[9]);
+ else
+ sprint(buf, "%s/queue.dump/%s%c%c", SPOOL, cp+4, cp[8], cp[9]);
+ cp = buf+strlen(buf);
+ if(access(buf, 0) < 0 && sysmkdir(buf, 0777) < 0)
+ return "/dev/null";
+ h = 0;
+ while(*sender)
+ h = h*257 + *sender++;
+ for(i = 0; i < 50; i++){
+ h += lrand();
+ sprint(cp, "/%lud", h);
+ if(access(buf, 0) >= 0)
+ continue;
+ fd = syscreate(buf, ORDWR, 0666);
+ if(fd >= 0){
+ if(debug)
+ fprint(2, "saving in %s\n", buf);
+ close(fd);
+ return buf;
+ }
+ }
+ }
+ return "/dev/null";
+}
+
+char *validator = "/mail/lib/validateaddress";
+
+int
+recipok(char *user)
+{
+ char *cp, *p, c;
+ char buf[512];
+ int n;
+ Biobuf *bp;
+ int pid;
+ Waitmsg *w;
+
+ if(shellchars(user)){
+ syslog(0, "smtpd", "shellchars in user name");
+ return 0;
+ }
+
+ if(access(validator, AEXEC) == 0)
+ switch(pid = fork()) {
+ case -1:
+ break;
+ case 0:
+ execl(validator, "validateaddress", user, nil);
+ exits(0);
+ default:
+ while(w = wait()) {
+ if(w->pid != pid)
+ continue;
+ if(w->msg[0] != 0){
+ /*
+ syslog(0, "smtpd", "validateaddress %s: %s", user, w->msg);
+ */
+ return 0;
+ }
+ break;
+ }
+ }
+
+ snprint(buf, sizeof(buf), "%s/names.blocked", UPASLIB);
+ bp = sysopen(buf, "r", 0);
+ if(bp == 0)
+ return 1;
+ for(;;){
+ cp = Brdline(bp, '\n');
+ if(cp == 0)
+ break;
+ n = Blinelen(bp);
+ cp[n-1] = 0;
+
+ while(*cp == ' ' || *cp == '\t')
+ cp++;
+ for(p = cp; c = *p; p++){
+ if(c == '#')
+ break;
+ if(c == ' ' || c == '\t')
+ break;
+ }
+ if(p > cp){
+ *p = 0;
+ if(cistrcmp(user, cp) == 0){
+ syslog(0, "smtpd", "names.blocked blocks %s", user);
+ Bterm(bp);
+ return 0;
+ }
+ }
+ }
+ Bterm(bp);
+ return 1;
+}
+
+/*
+ * a user can opt out of spam filtering by creating
+ * a file in his mail directory named 'nospamfiltering'.
+ */
+int
+optoutofspamfilter(char *addr)
+{
+ char *p, *f;
+ int rv;
+
+ p = strchr(addr, '!');
+ if(p)
+ p++;
+ else
+ p = addr;
+
+
+ rv = 0;
+ f = smprint("/mail/box/%s/nospamfiltering", p);
+ if(f != nil){
+ rv = access(f, 0)==0;
+ free(f);
+ }
+
+ return rv;
+}