From 5cdb17983ae6e6367ad7a940cb219eab247a9304 Mon Sep 17 00:00:00 2001 From: rsc Date: Sat, 29 Oct 2005 16:26:44 +0000 Subject: Thanks to John Cummings. --- src/cmd/upas/smtp/smtpd.c | 1494 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1494 insertions(+) create mode 100644 src/cmd/upas/smtp/smtpd.c (limited to 'src/cmd/upas/smtp/smtpd.c') diff --git a/src/cmd/upas/smtp/smtpd.c b/src/cmd/upas/smtp/smtpd.c new file mode 100644 index 00000000..7042c37c --- /dev/null +++ b/src/cmd/upas/smtp/smtpd.c @@ -0,0 +1,1494 @@ +#include "common.h" +#include "smtpd.h" +#include "smtp.h" +#include +#include +#include +#include +#include +#include +#include "../smtp/y.tab.h" + +#define DBGMX 1 + +char *me; +char *him=""; +char *dom; +process *pp; +String *mailer; +NetConnInfo *nci; + +int filterstate = ACCEPT; +int trusted; +int logged; +int rejectcount; +int hardreject; + +Biobuf bin; + +int debug; +int Dflag; +int fflag; +int gflag; +int rflag; +int sflag; +int authenticate; +int authenticated; +int passwordinclear; +char *tlscert; + +List senders; +List rcvers; + +char pipbuf[ERRMAX]; +char *piperror; +int pipemsg(int*); +String* startcmd(void); +int rejectcheck(void); +String* mailerpath(char*); + +static int +catchalarm(void *a, char *msg) +{ + int rv = 1; + + USED(a); + + /* log alarms but continue */ + if(strstr(msg, "alarm")){ + if(senders.first && rcvers.first) + syslog(0, "smtpd", "note: %s->%s: %s", s_to_c(senders.first->p), + s_to_c(rcvers.first->p), msg); + else + syslog(0, "smtpd", "note: %s", msg); + rv = 0; + } + + /* kill the children if there are any */ + if(pp) + syskillpg(pp->pid); + + return rv; +} + + /* override string error functions to do something reasonable */ +void +s_error(char *f, char *status) +{ + char errbuf[Errlen]; + + errbuf[0] = 0; + rerrstr(errbuf, sizeof(errbuf)); + if(f && *f) + reply("452 out of memory %s: %s\r\n", f, errbuf); + else + reply("452 out of memory %s\r\n", errbuf); + syslog(0, "smtpd", "++Malloc failure %s [%s]", him, nci->rsys); + exits(status); +} + +void +main(int argc, char **argv) +{ + char *p, buf[1024]; + char *netdir; + + netdir = nil; + quotefmtinstall(); + ARGBEGIN{ + case 'D': + Dflag++; + break; + case 'd': + debug++; + break; + case 'n': /* log peer ip address */ + netdir = ARGF(); + break; + case 'f': /* disallow relaying */ + fflag = 1; + break; + case 'g': + gflag = 1; + break; + case 'h': /* default domain name */ + dom = ARGF(); + break; + case 'k': /* prohibited ip address */ + p = ARGF(); + if (p) + addbadguy(p); + break; + case 'm': /* set mail command */ + p = ARGF(); + if(p) + mailer = mailerpath(p); + break; + case 'r': + rflag = 1; /* verify sender's domain */ + break; + case 's': /* save blocked messages */ + sflag = 1; + break; + case 'a': + authenticate = 1; + break; + case 'p': + passwordinclear = 1; + break; + case 'c': + tlscert = ARGF(); + 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: + fprint(2, "usage: smtpd [-dfhrs] [-n net] [-c cert]\n"); + exits("usage"); + }ARGEND; + + nci = getnetconninfo(netdir, 0); + if(nci == nil) + sysfatal("can't get remote system's address"); + + if(mailer == nil) + mailer = mailerpath("send"); + + if(debug){ + close(2); + snprint(buf, sizeof(buf), "%s/smtpd", UPASLOG); + if (open(buf, OWRITE) >= 0) { + seek(2, 0, 2); + fprint(2, "%d smtpd %s\n", getpid(), thedate()); + } else + debug = 0; + } + getconf(); + Binit(&bin, 0, OREAD); + + chdir(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; + Link *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(Link)); + lp->p = path; + lp->next = 0; + + if(l->last) + l->last->next = lp; + else + l->first = lp; + l->last = lp; +} + +#define SIZE 4096 +int +reply(char *fmt, ...) +{ + char buf[SIZE], *out; + va_list arg; + int n; + + va_start(arg, fmt); + out = vseprint(buf, buf+SIZE, fmt, arg); + va_end(arg); + n = (long)(out-buf); + if(debug) { + seek(2, 0, 2); + write(2, buf, n); + } + write(1, buf, n); + return n; +} + +void +reset(void) +{ + if(rejectcheck()) + return; + listfree(&rcvers); + listfree(&senders); + if(filterstate != DIALUP){ + logged = 0; + filterstate = ACCEPT; + } + reply("250 ok\r\n"); +} + +void +sayhi(void) +{ + reply("220 %s SMTP\r\n", dom); +} + +void +hello(String *himp, int extended) +{ + char **mynames; + + 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); + reply("554 Liar!\r\n"); + exits("client pretended to be us"); + return; + } + } + } + /* + * it is never acceptable to claim to be "localhost", + * "localhost.localdomain" or "localhost.example.com"; only spammers + * do this. it should be 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 software gets this wrong. + */ + if (strcmp(him, "localhost") == 0 || + strcmp(him, "localhost.localdomain") == 0 || + strcmp(him, "localhost.example.com") == 0) + goto Liarliar; + if(strchr(him, '.') == 0 && nci != nil && strchr(nci->rsys, '.') != nil) + him = nci->rsys; + + if(Dflag) + sleep(15*1000); + reply("250%c%s you are %s\r\n", extended ? '-' : ' ', dom, him); + if (extended) { + 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 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("503 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 Sender domain must exist: %s\r\n", s_to_c(path)); + return; + } + free(lastsender); /* different sender domain */ + lastsender = 0; + } + + /* + * see if this ip address, domain name, user name or account is blocked + */ + filterstate = blocked(path); + + logged = 0; + listadd(&senders, path); + reply("250 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 uchar rsysip[IPaddrlen]; + +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; + + fmtinstall('I', eipfmt); + parseip(rsysip, nci->rsys); + + /* + * 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 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 = ""; + + if(!recipok(s_to_c(path))){ + rejectcount++; + syslog(0, "smtpd", "Disallowed %s (%s/%s) to blocked name %s", + sender, him, nci->rsys, s_to_c(path)); + reply("550 %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 %s ... sending system not allowed\r\n", rcpt); + return; + } + + logged = 0; + /* forwarding() can modify 'path' on loopback request */ + if(filterstate == ACCEPT && (fflag && !authenticated) && forwarding(path)) { + syslog(0, "smtpd", "Bad Forward %s (%s/%s) (%s)", + s_to_c(senders.last->p), him, nci->rsys, s_to_c(path)); + rejectcount++; + reply("550 we don't relay. send to your-path@[] for loopback.\r\n"); + return; + } + listadd(&rcvers, path); + reply("250 receiver is %s\r\n", s_to_c(path)); +} + +void +quit(void) +{ + reply("221 Successful termination\r\n"); + close(0); + exits(0); +} + +void +turn(void) +{ + if(rejectcheck()) + return; + reply("502 TURN unimplemented\r\n"); +} + +void +noop(void) +{ + if(rejectcheck()) + return; + reply("250 Stop wasting my time!\r\n"); +} + +void +help(String *cmd) +{ + if(rejectcheck()) + return; + if(cmd) + s_free(cmd); + reply("250 Read rfc821 and stop wasting my time\r\n"); +} + +void +verify(String *path) +{ + char *p, *q; + char *av[4]; + + if(rejectcheck()) + return; + if(shellchars(s_to_c(path))){ + reply("503 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 We're busy right now, try later\r\n"); + return; + } + + p = Brdline(pp->std[1]->fp, '\n'); + if(p == 0){ + reply("550 String does not match anything.\r\n"); + } else { + p[Blinelen(pp->std[1]->fp)-1] = 0; + if(strchr(p, ':')) + reply("550 String does not match anything.\r\n"); + else{ + q = strrchr(p, '!'); + if(q) + p = q+1; + reply("250 %s <%s@%s>\r\n", s_to_c(path), p, dom); + } + } + proc_wait(pp); + proc_free(pp); + pp = 0; +} + +/* + * get a line that ends in crnl or cr, turn terminating crnl into a nl + * + * return 0 on EOF + */ +static int +getcrnl(String *s, Biobuf *fp) +{ + int c; + + for(;;){ + c = Bgetc(fp); + if(debug) { + seek(2, 0, 2); + fprint(2, "%c", c); + } + switch(c){ + case -1: + goto out; + case '\r': + c = Bgetc(fp); + if(c == '\n'){ + if(debug) { + seek(2, 0, 2); + fprint(2, "%c", c); + } + s_putc(s, '\n'); + goto out; + } + Bungetc(fp); + s_putc(s, '\r'); + break; + case '\n': + s_putc(s, c); + goto out; + default: + s_putc(s, c); + break; + } + } +out: + s_terminate(s); + return s_len(s); +} + +void +logcall(int nbytes) +{ + Link *l; + String *to, *from; + + to = s_new(); + from = s_new(); + for(l = senders.first; l; l = l->next){ + if(l != senders.first) + s_append(from, ", "); + s_append(from, s_to_c(l->p)); + } + for(l = rcvers.first; l; l = l->next){ + if(l != rcvers.first) + s_append(to, ", "); + s_append(to, s_to_c(l->p)); + } + syslog(0, "smtpd", "[%s/%s] %s sent %d bytes to %s", him, nci->rsys, + s_to_c(from), nbytes, s_to_c(to)); + s_free(to); + s_free(from); +} + +static void +logmsg(char *action) +{ + Link *l; + + if(logged) + return; + + logged = 1; + for(l = rcvers.first; l; l = l->next) + syslog(0, "smtpd", "%s %s (%s/%s) (%s)", action, + s_to_c(senders.last->p), him, nci->rsys, s_to_c(l->p)); +} + +static int +optoutall(int filterstate) +{ + Link *l; + + switch(filterstate){ + case ACCEPT: + case TRUSTED: + return filterstate; + } + + for(l = rcvers.first; l; l = l->next) + if(!optoutofspamfilter(s_to_c(l->p))) + return filterstate; + + return ACCEPT; +} + +String* +startcmd(void) +{ + int n; + Link *l; + char **av; + String *cmd; + char *filename; + + /* + * 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 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-We don't accept mail from %s.\r\n", s_to_c(senders.last->p)); + reply("554 Contact postmaster@%s for more information.\r\n", dom); + return 0; + case REFUSED: + logmsg("Refused"); + rejectcount++; + reply("554 Sender domain must exist: %s\r\n", s_to_c(senders.last->p)); + return 0; + default: + case NONE: + logmsg("Confused"); + rejectcount++; + reply("554-We have had an internal mailer error classifying your message.\r\n"); + reply("554-Filterstate is %d\r\n", filterstate); + reply("554 Contact postmaster@%s for more information.\r\n", dom); + return 0; + case ACCEPT: + case TRUSTED: + /* + * now that all other filters have been passed, + * do grey-list processing. + */ + if(gflag) + vfysenderhostok(); + + /* + * set up mail command + */ + cmd = s_clone(mailer); + n = 3; + for(l = rcvers.first; l; l = l->next) + n++; + av = malloc(n*sizeof(char*)); + if(av == nil){ + reply("450 We're busy right now, try later\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 We're busy right now, try later\n"); + s_free(cmd); + return 0; + } + return cmd; +} + +/* + * print out a header line, expanding any domainless addresses into + * address@him + */ +char* +bprintnode(Biobuf *b, Node *p) +{ + if(p->s){ + if(p->addr && strchr(s_to_c(p->s), '@') == nil){ + if(Bprint(b, "%s@%s", s_to_c(p->s), him) < 0) + return nil; + } else { + if(Bwrite(b, s_to_c(p->s), s_len(p->s)) < 0) + return nil; + } + }else{ + if(Bputc(b, p->c) < 0) + return nil; + } + if(p->white) + if(Bwrite(b, s_to_c(p->white), s_len(p->white)) < 0) + return nil; + return p->end+1; +} + +static String* +getaddr(Node *p) +{ + for(; p; p = p->next) + if(p->s && p->addr) + return p->s; + return nil; +} + +/* + * add waring headers of the form + * X-warning: + * for any headers that looked like they might be forged. + * + * return byte count of new headers + */ +static int +forgedheaderwarnings(void) +{ + int nbytes; + Field *f; + + nbytes = 0; + + /* warn about envelope sender */ + if(strcmp(s_to_c(senders.last->p), "/dev/null") != 0 && masquerade(senders.last->p, nil)) + nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect envelope domain\n"); + + /* + * check Sender: field. If it's OK, ignore the others because this is an + * exploded mailing list. + */ + for(f = firstfield; f; f = f->next){ + if(f->node->c == SENDER){ + if(masquerade(getaddr(f->node), him)) + nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect Sender: domain\n"); + else + return nbytes; + } + } + + /* check From: */ + for(f = firstfield; f; f = f->next){ + if(f->node->c == FROM && masquerade(getaddr(f->node), him)) + nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect From: domain\n"); + } + return nbytes; +} + +/* + * pipe message to mailer with the following transformations: + * - change \r\n into \n. + * - add sender's domain to any addrs with no domain + * - add a From: if none of From:, Sender:, or Replyto: exists + * - add a Received: line + */ +int +pipemsg(int *byteswritten) +{ + int status; + char *cp; + String *line; + String *hdr; + int n, nbytes; + int sawdot; + Field *f; + Node *p; + Link *l; + + pipesig(&status); /* set status to 1 on write to closed pipe */ + sawdot = 0; + status = 0; + + /* + * add a 'From ' line as envelope + */ + nbytes = 0; + nbytes += Bprint(pp->std[0]->fp, "From %s %s remote from \n", + s_to_c(senders.first->p), thedate()); + + /* + * add our own Received: stamp + */ + nbytes += Bprint(pp->std[0]->fp, "Received: from %s ", him); + if(nci->rsys) + nbytes += Bprint(pp->std[0]->fp, "([%s]) ", nci->rsys); + nbytes += Bprint(pp->std[0]->fp, "by %s; %s\n", me, thedate()); + + /* + * read first 16k obeying '.' escape. we're assuming + * the header will all be there. + */ + line = s_new(); + hdr = s_new(); + while(sawdot == 0 && s_len(hdr) < 16*1024){ + n = getcrnl(s_reset(line), &bin); + + /* eof or error ends the message */ + if(n <= 0) + break; + + /* a line with only a '.' ends the message */ + cp = s_to_c(line); + if(n == 2 && *cp == '.' && *(cp+1) == '\n'){ + sawdot = 1; + break; + } + + s_append(hdr, *cp == '.' ? cp+1 : cp); + } + + /* + * parse header + */ + yyinit(s_to_c(hdr), s_len(hdr)); + yyparse(); + + /* + * Look for masquerades. Let Sender: trump From: to allow mailing list + * forwarded messages. + */ + if(fflag) + nbytes += forgedheaderwarnings(); + + /* + * add an orginator and/or destination if either is missing + */ + if(originator == 0){ + if(senders.last == nil) + Bprint(pp->std[0]->fp, "From: /dev/null@%s\n", him); + else + Bprint(pp->std[0]->fp, "From: %s\n", s_to_c(senders.last->p)); + } + if(destination == 0){ + Bprint(pp->std[0]->fp, "To: "); + for(l = rcvers.first; l; l = l->next){ + if(l != rcvers.first) + Bprint(pp->std[0]->fp, ", "); + Bprint(pp->std[0]->fp, "%s", s_to_c(l->p)); + } + Bprint(pp->std[0]->fp, "\n"); + } + + /* + * add sender's domain to any domainless addresses + * (to avoid forging local addresses) + */ + cp = s_to_c(hdr); + for(f = firstfield; cp != nil && f; f = f->next){ + for(p = f->node; cp != 0 && p; p = p->next) + cp = bprintnode(pp->std[0]->fp, p); + if(status == 0 && Bprint(pp->std[0]->fp, "\n") < 0){ + piperror = "write error"; + status = 1; + } + } + if(cp == nil){ + piperror = "sender domain"; + status = 1; + } + + /* write anything we read following the header */ + if(status == 0 && Bwrite(pp->std[0]->fp, cp, s_to_c(hdr) + s_len(hdr) - cp) < 0){ + piperror = "write error 2"; + status = 1; + } + s_free(hdr); + + /* + * pass rest of message to mailer. take care of '.' + * escapes. + */ + while(sawdot == 0){ + n = getcrnl(s_reset(line), &bin); + + /* eof or error ends the message */ + if(n <= 0) + break; + + /* a line with only a '.' ends the message */ + cp = s_to_c(line); + if(n == 2 && *cp == '.' && *(cp+1) == '\n'){ + sawdot = 1; + break; + } + nbytes += n; + if(status == 0 && Bwrite(pp->std[0]->fp, *cp == '.' ? cp+1 : cp, n) < 0){ + piperror = "write error 3"; + status = 1; + } + } + s_free(line); + if(sawdot == 0){ + /* message did not terminate normally */ + snprint(pipbuf, sizeof pipbuf, "network eof: %r"); + piperror = pipbuf; + syskillpg(pp->pid); + status = 1; + } + + if(status == 0 && Bflush(pp->std[0]->fp) < 0){ + piperror = "write error 4"; + status = 1; + } + stream_free(pp->std[0]); + pp->std[0] = 0; + *byteswritten = nbytes; + pipesigoff(); + if(status && !piperror) + piperror = "write on closed pipe"; + return status; +} + +char* +firstline(char *x) +{ + static char buf[128]; + char *p; + + strncpy(buf, x, sizeof(buf)); + buf[sizeof(buf)-1] = 0; + p = strchr(buf, '\n'); + if(p) + *p = 0; + return buf; +} + +int +sendermxcheck(void) +{ + char *cp, *senddom, *user; + char *who; + int pid; + 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) +{ + String *cmd; + String *err; + int status, nbytes; + char *cp, *ep; + char errx[ERRMAX]; + Link *l; + + if(rejectcheck()) + return; + if(senders.last == 0){ + reply("503 Data without MAIL FROM:\r\n"); + rejectcount++; + return; + } + if(rcvers.last == 0){ + reply("503 Data without RCPT TO:\r\n"); + rejectcount++; + return; + } + if(sendermxcheck()){ + rerrstr(errx, sizeof errx); + if(strncmp(errx, "rejected:", 9) == 0) + reply("554 %s\r\n", errx); + else + reply("450 %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 .\r\n"); + + /* + * allow 145 more minutes to move the data + */ + alarm(145*60*1000); + + status = pipemsg(&nbytes); + + /* + * read any error messages + */ + err = s_new(); + while(s_read_line(pp->std[2]->fp, err)) + ; + + alarm(0); + atnotify(catchalarm, 0); + + status |= proc_wait(pp); + if(debug){ + seek(2, 0, 2); + fprint(2, "%d status %ux\n", getpid(), status); + if(*s_to_c(err)) + fprint(2, "%d error %s\n", getpid(), s_to_c(err)); + } + + /* + * if process terminated abnormally, send back error message + */ + if(status){ + int code; + + 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; + } 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; + } + for(cp = s_to_c(err); ep = strchr(cp, '\n'); cp = ep){ + *ep++ = 0; + reply("%d-%s\r\n", code, cp); + } + reply("%d mail process terminated abnormally\r\n", code); + } else { + if(filterstate == BLOCKED) + reply("554 we believe this is spam. we don't accept it.\r\n"); + else + if(filterstate == DELAY) + reply("554 There will be a delay in delivery of this message.\r\n"); + else { + reply("250 sent\r\n"); + logcall(nbytes); + } + } + proc_free(pp); + pp = 0; + s_free(cmd); + s_free(err); + + listfree(&senders); + listfree(&rcvers); +} + +/* + * when we have blocked a transaction based on IP address, there is nothing + * that the sender can do to convince us to take the message. after the + * first rejection, some spammers continually RSET and give a new MAIL FROM: + * filling our logs with rejections. rejectcheck() limits the retries and + * swiftly rejects all further commands after the first 500-series message + * is issued. + */ +int +rejectcheck(void) +{ + + if(rejectcount > MAXREJECTS){ + syslog(0, "smtpd", "Rejected (%s/%s)", him, nci->rsys); + reply("554 too many errors. transaction failed.\r\n"); + exits("errcount"); + } + if(hardreject){ + rejectcount++; + reply("554 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) +{ + String *sout; + int lin, lout; + 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) +{ + uchar *cert; + int certlen, fd; + TLSconn *conn; + + conn = mallocz(sizeof *conn, 1); + cert = readcert(tlscert, &certlen); + if (conn == nil || cert == nil) { + if (conn != nil) + free(conn); + reply("454 TLS not available\r\n"); + return; + } + reply("220 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) +{ + Chalstate *chs = nil; + AuthInfo *ai = nil; + String *s_resp1_64 = nil; + String *s_resp2_64 = nil; + String *s_resp1 = nil; + String *s_resp2 = nil; + char *scratch = nil; + char *user, *pass; + + 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 Bad sequence of commands\r\n"); + goto bomb_out; + } + if (cistrcmp(s_to_c(mech), "plain") == 0) { + + if (!passwordinclear) { + rejectcount++; + reply("538 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 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 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 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) + reply("235 Authentication successful\r\n"); + else { + rejectcount++; + reply("535 Authentication failed\r\n"); + } + goto bomb_out; + } + else if (cistrcmp(s_to_c(mech), "cram-md5") == 0) { + char *resp; + int chal64n; + char *t; + + chs = auth_challenge("proto=cram role=server"); + if (chs == nil) { + rejectcount++; + reply("501 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 Cannot decode base64\r\n"); + goto bomb_out; + } + /* should be of form */ + resp = s_to_c(s_resp1); + t = strchr(resp, ' '); + if (t == nil) { + rejectcount++; + reply("501 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 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); +} -- cgit v1.2.3