| #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 <thread.h> |
| #include "../smtp/rfc822.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); |
| threadexitsall(status); |
| } |
| |
| void |
| threadmain(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': |
| fprint(2, "tls is not available\n"); |
| threadexitsall("no tls"); |
| 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"); |
| threadexitsall("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.db", 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(); |
| threadexitsall(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"); |
| threadexitsall("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 = "<unknown>"; |
| |
| 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); |
| threadexitsall(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: <reason> |
| * for any headers that looked like they might be forged. |
| * |
| * return byte count of new headers |
| */ |
| static int |
| forgedheaderwarnings(void) |
| { |
| int nbytes; |
| Field *f; |
| |
| nbytes = 0; |
| |
| /* warn about envelope sender */ |
| if(strcmp(s_to_c(senders.last->p), "/dev/null") != 0 && masquerade(senders.last->p, nil)) |
| nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect envelope domain\n"); |
| |
| /* |
| * check Sender: field. If it's OK, ignore the others because this is an |
| * exploded mailing list. |
| */ |
| for(f = firstfield; f; f = f->next){ |
| if(f->node->c == SENDER){ |
| if(masquerade(getaddr(f->node), him)) |
| nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect Sender: domain\n"); |
| else |
| return nbytes; |
| } |
| } |
| |
| /* check From: */ |
| for(f = firstfield; f; f = f->next){ |
| if(f->node->c == FROM && masquerade(getaddr(f->node), him)) |
| nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect From: domain\n"); |
| } |
| return nbytes; |
| } |
| |
| /* |
| * pipe message to mailer with the following transformations: |
| * - change \r\n into \n. |
| * - add sender's domain to any addrs with no domain |
| * - add a From: if none of From:, Sender:, or Replyto: exists |
| * - add a Received: line |
| */ |
| int |
| pipemsg(int *byteswritten) |
| { |
| int 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; |
| static char *validate; |
| |
| 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(validate == nil) |
| validate = unsharp("#9/mail/lib/validatesender"); |
| if(access(validate, 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(validate, "validatesender", |
| "-n", nci->root, senddom, user, nil); |
| threadexitsall("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 <CRLF>.<CRLF>\r\n"); |
| |
| /* |
| * allow 145 more minutes to move the data |
| */ |
| alarm(145*60*1000); |
| |
| status = pipemsg(&nbytes); |
| |
| /* |
| * read any error messages |
| */ |
| err = s_new(); |
| while(s_read_line(pp->std[2]->fp, err)) |
| ; |
| |
| alarm(0); |
| atnotify(catchalarm, 0); |
| |
| status |= proc_wait(pp); |
| if(debug){ |
| seek(2, 0, 2); |
| fprint(2, "%d status %ux\n", getpid(), status); |
| if(*s_to_c(err)) |
| fprint(2, "%d error %s\n", getpid(), s_to_c(err)); |
| } |
| |
| /* |
| * if process terminated abnormally, send back error message |
| */ |
| if(status){ |
| int code; |
| |
| 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 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 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"); |
| threadexitsall("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); |
| threadexitsall("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 <user><space><response> */ |
| 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); |
| } |