aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/upas/smtp/smtpd.c
diff options
context:
space:
mode:
authorrsc <devnull@localhost>2005-10-29 16:26:44 +0000
committerrsc <devnull@localhost>2005-10-29 16:26:44 +0000
commit5cdb17983ae6e6367ad7a940cb219eab247a9304 (patch)
tree8ca1ef49af2a96e7daebe624d91fdf679814a057 /src/cmd/upas/smtp/smtpd.c
parentcd3745196389579fb78b9b01ef1daefb5a57aa71 (diff)
downloadplan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.tar.gz
plan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.tar.bz2
plan9port-5cdb17983ae6e6367ad7a940cb219eab247a9304.zip
Thanks to John Cummings.
Diffstat (limited to 'src/cmd/upas/smtp/smtpd.c')
-rw-r--r--src/cmd/upas/smtp/smtpd.c1494
1 files changed, 1494 insertions, 0 deletions
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 <ctype.h>
+#include <ip.h>
+#include <ndb.h>
+#include <mp.h>
+#include <libsec.h>
+#include <auth.h>
+#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 = "<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);
+ 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: <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;
+
+ 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 <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(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 <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);
+}