diff options
author | Taru Karttunen <taruti@taruti.net> | 2011-03-30 15:46:40 +0300 |
---|---|---|
committer | Taru Karttunen <taruti@taruti.net> | 2011-03-30 15:46:40 +0300 |
commit | e5888a1ffdae813d7575f5fb02275c6bb07e5199 (patch) | |
tree | d8d51eac403f07814b9e936eed0c9a79195e2450 /sys/src/cmd/upas/smtp |
Import sources from 2011-03-30 iso image
Diffstat (limited to 'sys/src/cmd/upas/smtp')
-rwxr-xr-x | sys/src/cmd/upas/smtp/greylist.c | 317 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/mkfile | 53 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/mxdial.c | 359 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/rfc822.y | 779 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/rmtdns.c | 59 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/smtp.c | 1119 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/smtp.h | 67 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/smtpd.c | 1736 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/smtpd.h | 69 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/smtpd.y | 318 | ||||
-rwxr-xr-x | sys/src/cmd/upas/smtp/spam.c | 594 |
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; +} |