ref: 985b2457cda207c2c65edeb647aedbb6e92dac46
dir: /sys/src/cmd/upas/smtp/smtp.c/
#include "common.h" #include "smtp.h" #include <ctype.h> #include <mp.h> #include <libsec.h> #include <auth.h> static char* connect(char*); static char* wraptls(void); static char* dotls(char*); static char* doauth(char*); void addhostdom(String*, char*); String* bangtoat(char*); String* convertheader(String*); int dBprint(char*, ...); int dBputc(int); char* data(String*, Biobuf*); char* domainify(char*, char*); String* fixrouteaddr(String*, Node*, Node*); char* getcrnl(String*); int getreply(void); char* hello(char*, int); char* mailfrom(char*); int printdate(Node*); int printheader(void); void putcrnl(char*, int); void quit(char*); char* rcptto(char*); char* rewritezone(char *); #define Retry "Retry, Temporary Failure" #define Giveup "Permanent Failure" String *reply; /* last reply */ String *toline; int alarmscale; int autistic; int debug; /* true if we're debugging */ int filter; int insecure; int last = 'n'; /* last character sent by putcrnl() */ int ping; int quitting; /* when error occurs in quit */ int tryauth; /* Try to authenticate, if supported */ int trysecure; /* Try to use TLS if the other side supports it */ char *quitrv; /* deferred return value when in quit */ char ddomain[1024]; /* domain name of destination machine */ char *gdomain; /* domain name of gateway */ char *uneaten; /* first character after rfc822 headers */ char *farend; /* system we are trying to send to */ char *user; /* user we are authenticating as, if authenticating */ char hostdomain[256]; Biobuf bin; Biobuf bout; Biobuf berr; Biobuf bfile; static int bustedmx; void usage(void) { fprint(2, "usage: smtp [-aAdfipst] [-b busted-mx] [-g gw] [-h host] " "[-u user] [.domain] net!host[!service] sender rcpt-list\n"); exits(Giveup); } int timeout(void *x, char *msg) { USED(x); syslog(0, "smtp.fail", "interrupt: %s: %s", farend, msg); if(strstr(msg, "alarm")){ fprint(2, "smtp timeout: connection to %s timed out\n", farend); if(quitting) exits(quitrv); exits(Retry); } if(strstr(msg, "closed pipe")){ /* call _exits() to prevent Bio from trying to flush closed pipe */ fprint(2, "smtp timeout: connection closed to %s\n", farend); if(quitting){ syslog(0, "smtp.fail", "closed pipe to %s", farend); _exits(quitrv); } _exits(Retry); } return 0; } void removenewline(char *p) { int n = strlen(p)-1; if(n < 0) return; if(p[n] == '\n') p[n] = 0; } void main(int argc, char **argv) { int i, ok, rcvrs; char *addr, *rv, *trv, *host, *domain; char **errs; char hellodomain[256]; String *from, *fromm, *sender; alarmscale = 60*1000; /* minutes */ quotefmtinstall(); fmtinstall('[', encodefmt); errs = malloc(argc*sizeof(char*)); reply = s_new(); host = 0; ARGBEGIN{ case 'a': tryauth = 1; if(trysecure == 0) trysecure = 1; break; case 'A': /* autistic: won't talk to us until we talk (Verizon) */ autistic = 1; break; case 'b': if (bustedmx >= Maxbustedmx) sysfatal("more than %d busted mxs given", Maxbustedmx); bustedmxs[bustedmx++] = EARGF(usage()); break; case 'd': debug = 1; break; case 'f': filter = 1; break; case 'g': gdomain = EARGF(usage()); break; case 'h': host = EARGF(usage()); break; case 'i': insecure = 1; break; case 'p': alarmscale = 10*1000; /* tens of seconds */ ping = 1; break; case 's': if(trysecure == 0) trysecure = 1; break; case 't': trysecure = 2; break; case 'u': user = EARGF(usage()); break; default: usage(); break; }ARGEND; Binit(&berr, 2, OWRITE); Binit(&bfile, 0, OREAD); /* * get domain and add to host name */ if(*argv && **argv=='.') { domain = *argv; argv++; argc--; } else domain = domainname_read(); if(host == 0) host = sysname_read(); strcpy(hostdomain, domainify(host, domain)); strcpy(hellodomain, domainify(sysname_read(), domain)); /* * get destination address */ if(*argv == 0) usage(); addr = *argv++; argc--; farend = addr; /* * get sender's machine. * get sender in internet style. domainify if necessary. */ if(*argv == 0) usage(); sender = unescapespecial(s_copy(*argv++)); argc--; fromm = s_clone(sender); rv = strrchr(s_to_c(fromm), '!'); if(rv) *rv = 0; else *s_to_c(fromm) = 0; from = bangtoat(s_to_c(sender)); /* * send the mail */ if(filter){ Binit(&bout, 1, OWRITE); rv = data(from, &bfile); if(rv != 0) goto error; exits(0); } /* mxdial uses its own timeout handler */ if((rv = connect(addr)) != 0) exits(rv); /* 10 minutes to get through the initial handshake */ atnotify(timeout, 1); alarm(10*alarmscale); if((rv = hello(hellodomain, 0)) != 0) goto error; alarm(10*alarmscale); if((rv = mailfrom(s_to_c(from))) != 0) goto error; ok = 0; rcvrs = 0; /* if any rcvrs are ok, we try to send the message */ for(i = 0; i < argc; i++){ if((trv = rcptto(argv[i])) != 0){ /* remember worst error */ if(rv != Giveup) rv = trv; errs[rcvrs] = strdup(s_to_c(reply)); removenewline(errs[rcvrs]); } else { ok++; errs[rcvrs] = 0; } rcvrs++; } /* if no ok rcvrs or worst error is retry, give up */ if(ok == 0 || rv == Retry) goto error; if(ping){ quit(0); exits(0); } rv = data(from, &bfile); if(rv != 0) goto error; quit(0); if(rcvrs == ok) exits(0); /* * here when some but not all rcvrs failed */ fprint(2, "%s connect to %s:\n", thedate(), addr); for(i = 0; i < rcvrs; i++){ if(errs[i]){ syslog(0, "smtp.fail", "delivery to %s at %s failed: %s", argv[i], addr, errs[i]); fprint(2, " mail to %s failed: %s", argv[i], errs[i]); } } exits(Giveup); /* * here when all rcvrs failed */ error: removenewline(s_to_c(reply)); syslog(0, "smtp.fail", "%s to %s failed: %s", ping ? "ping" : "delivery", addr, s_to_c(reply)); fprint(2, "%s connect to %s:\n%s\n", thedate(), addr, s_to_c(reply)); if(!filter) quit(rv); exits(rv); } /* * connect to the remote host */ static char * connect(char* net) { char buf[Errlen]; int fd; fd = mxdial(net, ddomain, gdomain); if(fd < 0){ rerrstr(buf, sizeof(buf)); Bprint(&berr, "smtp: %s (%s)\n", buf, net); syslog(0, "smtp.fail", "%s (%s)", buf, net); if(strstr(buf, "illegal") || strstr(buf, "unknown") || strstr(buf, "can't translate")) return Giveup; else return Retry; } Binit(&bin, fd, OREAD); fd = dup(fd, -1); Binit(&bout, fd, OWRITE); return 0; } static char smtpthumbs[] = "/sys/lib/tls/smtp"; static char smtpexclthumbs[] = "/sys/lib/tls/smtp.exclude"; static char * wraptls(void) { TLSconn *c; Thumbprint *goodcerts; char *h, *err; int fd; uchar hash[SHA1dlen]; goodcerts = nil; err = Giveup; c = mallocz(sizeof(*c), 1); if (c == nil) return err; fd = tlsClient(Bfildes(&bout), c); if (fd < 0) { syslog(0, "smtp", "tlsClient to %q: %r", ddomain); goto Out; } Bterm(&bout); Binit(&bout, fd, OWRITE); fd = dup(fd, Bfildes(&bin)); Bterm(&bin); Binit(&bin, fd, OREAD); goodcerts = initThumbprints(smtpthumbs, smtpexclthumbs); if (goodcerts == nil) { syslog(0, "smtp", "bad thumbprints in %s", smtpthumbs); goto Out; } /* compute sha1 hash of remote's certificate, see if we know it */ sha1(c->cert, c->certlen, hash, nil); if (!okThumbprint(hash, goodcerts)) { /* TODO? if not excluded, add hash to thumb list */ h = malloc(2*sizeof hash + 1); if (h == nil) goto Out; enc16(h, 2*sizeof hash + 1, hash, sizeof hash); syslog(0, "smtp", "remote cert. has bad thumbprint: x509 sha1=%s server=%q", h, ddomain); free(h); goto Out; } syslog(0, "smtp", "started TLS to %q", ddomain); err = nil; Out: if(goodcerts != nil) freeThumbprints(goodcerts); free(c->cert); free(c->sessionID); free(c); return err; } /* * exchange names with remote host, attempt to * enable encryption and optionally authenticate. * not fatal if we can't. */ static char * dotls(char *me) { char *err; dBprint("STARTTLS\r\n"); if (getreply() != 2) return Giveup; err = wraptls(); if (err != nil) return err; return(hello(me, 1)); } static char * doauth(char *methods) { char *buf, *err; UserPasswd *p; int n; DS ds; dial_string_parse(ddomain, &ds); if(user != nil) p = auth_getuserpasswd(nil, "proto=pass service=smtp server=%q user=%q", ds.host, user); else p = auth_getuserpasswd(nil, "proto=pass service=smtp server=%q", ds.host); if (p == nil) return Giveup; err = Retry; if (strstr(methods, "LOGIN")){ dBprint("AUTH LOGIN\r\n"); if (getreply() != 3) goto out; dBprint("%.*[\r\n", strlen(p->user), p->user); if (getreply() != 3) goto out; dBprint("%.*[\r\n", strlen(p->passwd), p->passwd); if (getreply() != 2) goto out; err = nil; } else if (strstr(methods, "PLAIN")){ n = strlen(p->user) + strlen(p->passwd) + 2; buf = malloc(n+1); if (buf == nil) { free(buf); goto out; /* Out of memory */ } snprint(buf, n, "%c%s%c%s", 0, p->user, 0, p->passwd); dBprint("AUTH PLAIN %.*[\r\n", n, buf); memset(buf, 0, n); free(buf); if (getreply() != 2) goto out; err = nil; } else err = "No supported AUTH method"; out: memset(p->user, 0, strlen(p->user)); memset(p->passwd, 0, strlen(p->passwd)); free(p); return err; } char * hello(char *me, int encrypted) { int ehlo; String *r; char *ret, *s, *t; if (!encrypted) { if(trysecure > 1){ if((ret = wraptls()) != nil) return ret; encrypted = 1; } /* * Verizon fails to print the smtp greeting banner when it * answers a call. Send a no-op in the hope of making it * talk. */ if (autistic) { dBprint("NOOP\r\n"); getreply(); /* consume the smtp greeting */ /* next reply will be response to noop */ } switch(getreply()){ case 2: break; case 5: return Giveup; default: return Retry; } } ehlo = 1; Again: if(ehlo) dBprint("EHLO %s\r\n", me); else dBprint("HELO %s\r\n", me); switch (getreply()) { case 2: break; case 5: if(ehlo){ ehlo = 0; goto Again; } return Giveup; default: return Retry; } r = s_clone(reply); if(r == nil) return Retry; /* Out of memory or couldn't get string */ /* Invariant: every line has a newline, a result of getcrlf() */ for(s = s_to_c(r); (t = strchr(s, '\n')) != nil; s = t + 1){ *t = '\0'; for (t = s; *t != '\0'; t++) *t = toupper(*t); if(!encrypted && trysecure && (strcmp(s, "250-STARTTLS") == 0 || strcmp(s, "250 STARTTLS") == 0)){ s_free(r); return dotls(me); } if(tryauth && (encrypted || insecure) && (strncmp(s, "250 AUTH", strlen("250 AUTH")) == 0 || strncmp(s, "250-AUTH", strlen("250 AUTH")) == 0)){ ret = doauth(s + strlen("250 AUTH ")); s_free(r); return ret; } } s_free(r); return 0; } /* * report sender to remote */ char * mailfrom(char *from) { if(!returnable(from)) dBprint("MAIL FROM:<>\r\n"); else if(strchr(from, '@')) dBprint("MAIL FROM:<%s>\r\n", from); else dBprint("MAIL FROM:<%s@%s>\r\n", from, hostdomain); switch(getreply()){ case 2: break; case 5: return Giveup; default: return Retry; } return 0; } /* * report a recipient to remote */ char * rcptto(char *to) { String *s; s = unescapespecial(bangtoat(to)); if(toline == 0) toline = s_new(); else s_append(toline, ", "); s_append(toline, s_to_c(s)); if(strchr(s_to_c(s), '@')) dBprint("RCPT TO:<%s>\r\n", s_to_c(s)); else { s_append(toline, "@"); s_append(toline, ddomain); dBprint("RCPT TO:<%s@%s>\r\n", s_to_c(s), ddomain); } alarm(10*alarmscale); switch(getreply()){ case 2: break; case 5: return Giveup; default: return Retry; } return 0; } static char hex[] = "0123456789abcdef"; /* * send the damn thing */ char * data(String *from, Biobuf *b) { char *buf, *cp; int i, n, nbytes, bufsize, eof, r; String *fromline; char errmsg[Errlen]; char id[40]; /* * input the header. */ buf = malloc(1); if(buf == 0){ s_append(s_restart(reply), "out of memory"); return Retry; } n = 0; eof = 0; for(;;){ cp = Brdline(b, '\n'); if(cp == nil){ eof = 1; break; } nbytes = Blinelen(b); buf = realloc(buf, n+nbytes+1); if(buf == 0){ s_append(s_restart(reply), "out of memory"); return Retry; } strncpy(buf+n, cp, nbytes); n += nbytes; if(nbytes == 1) /* end of header */ break; } buf[n] = 0; bufsize = n; /* * parse the header, turn all addresses into @ format */ yyinit(buf, n); yyparse(); /* * print message observing '.' escapes and using \r\n for \n */ alarm(20*alarmscale); if(!filter){ dBprint("DATA\r\n"); switch(getreply()){ case 3: break; case 5: free(buf); return Giveup; default: free(buf); return Retry; } } /* * send header. add a message-id, a sender, and a date if there * isn't one */ nbytes = 0; fromline = convertheader(from); uneaten = buf; srand(truerand()); if(messageid == 0){ for(i=0; i<16; i++){ r = rand()&0xFF; id[2*i] = hex[r&0xF]; id[2*i+1] = hex[(r>>4)&0xF]; } id[2*i] = '\0'; nbytes += Bprint(&bout, "Message-ID: <%s@%s>\r\n", id, hostdomain); if(debug) Bprint(&berr, "Message-ID: <%s@%s>\r\n", id, hostdomain); } if(originator==0){ nbytes += Bprint(&bout, "From: %s\r\n", s_to_c(fromline)); if(debug) Bprint(&berr, "From: %s\r\n", s_to_c(fromline)); } s_free(fromline); if(destination == 0 && toline) if(*s_to_c(toline) == '@'){ /* route addr */ nbytes += Bprint(&bout, "To: <%s>\r\n", s_to_c(toline)); if(debug) Bprint(&berr, "To: <%s>\r\n", s_to_c(toline)); } else { nbytes += Bprint(&bout, "To: %s\r\n", s_to_c(toline)); if(debug) Bprint(&berr, "To: %s\r\n", s_to_c(toline)); } if(date==0 && udate) nbytes += printdate(udate); if (usys) uneaten = usys->end + 1; nbytes += printheader(); if (*uneaten != '\n') putcrnl("\n", 1); /* * send body */ putcrnl(uneaten, buf+n - uneaten); nbytes += buf+n - uneaten; if(eof == 0){ for(;;){ n = Bread(b, buf, bufsize); if(n < 0){ rerrstr(errmsg, sizeof(errmsg)); s_append(s_restart(reply), errmsg); free(buf); return Retry; } if(n == 0) break; alarm(10*alarmscale); putcrnl(buf, n); nbytes += n; } } free(buf); if(!filter){ if(last != '\n') dBprint("\r\n.\r\n"); else dBprint(".\r\n"); alarm(10*alarmscale); switch(getreply()){ case 2: break; case 5: return Giveup; default: return Retry; } syslog(0, "smtp", "%s sent %d bytes to %s", s_to_c(from), nbytes, s_to_c(toline));/**/ } return 0; } /* * we're leaving */ void quit(char *rv) { /* 60 minutes to quit */ quitting = 1; quitrv = rv; alarm(60*alarmscale); dBprint("QUIT\r\n"); getreply(); Bterm(&bout); Bterm(&bfile); } /* * read a reply into a string, return the reply code */ int getreply(void) { char *line; int rv; reply = s_reset(reply); for(;;){ line = getcrnl(reply); if(debug) Bflush(&berr); if(line == 0) return -1; if(!isdigit(line[0]) || !isdigit(line[1]) || !isdigit(line[2])) return -1; if(line[3] != '-') break; } if(debug) Bflush(&berr); rv = atoi(line)/100; return rv; } void addhostdom(String *buf, char *host) { s_append(buf, "@"); s_append(buf, host); } /* * Convert from `bang' to `source routing' format. * * a.x.y!b.p.o!c!d -> @a.x.y:[email protected] */ String * bangtoat(char *addr) { String *buf; register int i; int j, d; char *field[128]; /* parse the '!' format address */ buf = s_new(); for(i = 0; addr; i++){ field[i] = addr; addr = strchr(addr, '!'); if(addr) *addr++ = 0; } if (i==1) { s_append(buf, field[0]); return buf; } /* * count leading domain fields (non-domains don't count) */ for(d = 0; d<i-1; d++) if(strchr(field[d], '.')==0) break; /* * if there are more than 1 leading domain elements, * put them in as source routing */ if(d > 1){ addhostdom(buf, field[0]); for(j=1; j<d-1; j++){ s_append(buf, ","); s_append(buf, "@"); s_append(buf, field[j]); } s_append(buf, ":"); } /* * throw in the non-domain elements separated by '!'s */ s_append(buf, field[d]); for(j=d+1; j<=i-1; j++) { s_append(buf, "!"); s_append(buf, field[j]); } if(d) addhostdom(buf, field[d-1]); return buf; } /* * convert header addresses to @ format. * if the address is a source address, and a domain is specified, * make sure it falls in the domain. */ String* convertheader(String *from) { Field *f; Node *p, *lastp; String *a; if(!returnable(s_to_c(from))){ from = s_new(); s_append(from, "Postmaster"); addhostdom(from, hostdomain); } else if(strchr(s_to_c(from), '@') == 0){ a = username(from); if(a) { s_append(a, " <"); s_append(a, s_to_c(from)); addhostdom(a, hostdomain); s_append(a, ">"); from = a; } else { from = s_copy(s_to_c(from)); addhostdom(from, hostdomain); } } else from = s_copy(s_to_c(from)); for(f = firstfield; f; f = f->next){ lastp = 0; for(p = f->node; p; lastp = p, p = p->next){ if(!p->addr) continue; a = bangtoat(s_to_c(p->s)); s_free(p->s); if(strchr(s_to_c(a), '@') == 0) addhostdom(a, hostdomain); else if(*s_to_c(a) == '@') a = fixrouteaddr(a, p->next, lastp); p->s = a; } } return from; } /* * ensure route addr has brackets around it */ String* fixrouteaddr(String *raddr, Node *next, Node *last) { String *a; if(last && last->c == '<' && next && next->c == '>') return raddr; /* properly formed already */ a = s_new(); s_append(a, "<"); s_append(a, s_to_c(raddr)); s_append(a, ">"); s_free(raddr); return a; } /* * print out the parsed header */ int printheader(void) { int n, len; Field *f; Node *p; char *cp; char c[1]; n = 0; for(f = firstfield; f; f = f->next){ for(p = f->node; p; p = p->next){ if(p->s) n += dBprint("%s", s_to_c(p->s)); else { c[0] = p->c; putcrnl(c, 1); n++; } if(p->white){ cp = s_to_c(p->white); len = strlen(cp); putcrnl(cp, len); n += len; } uneaten = p->end; } putcrnl("\n", 1); n++; uneaten++; /* skip newline */ } return n; } /* * add a domain onto an name, return the new name */ char * domainify(char *name, char *domain) { static String *s; char *p; if(domain==0 || strchr(name, '.')!=0) return name; s = s_reset(s); s_append(s, name); p = strchr(domain, '.'); if(p == 0){ s_append(s, "."); p = domain; } s_append(s, p); return s_to_c(s); } /* * print message observing '.' escapes and using \r\n for \n */ void putcrnl(char *cp, int n) { int c; for(; n; n--, cp++){ c = *cp; if(c == '\n') dBputc('\r'); else if(c == '.' && last=='\n') dBputc('.'); dBputc(c); last = c; } } /* * Get a line including a crnl into a string. Convert crnl into nl. */ char * getcrnl(String *s) { int c; int count; count = 0; for(;;){ c = Bgetc(&bin); if(debug) Bputc(&berr, c); switch(c){ case -1: s_append(s, "connection closed unexpectedly by remote system"); s_terminate(s); return 0; case '\r': c = Bgetc(&bin); if(c == '\n'){ case '\n': s_putc(s, c); if(debug) Bputc(&berr, c); count++; s_terminate(s); return s->ptr - count; } Bungetc(&bin); s_putc(s, '\r'); if(debug) Bputc(&berr, '\r'); count++; break; default: s_putc(s, c); count++; break; } } } /* * print out a parsed date */ int printdate(Node *p) { int n, sep = 0; n = dBprint("Date: %s,", s_to_c(p->s)); for(p = p->next; p; p = p->next){ if(p->s){ if(sep == 0) { dBputc(' '); n++; } if (p->next) n += dBprint("%s", s_to_c(p->s)); else n += dBprint("%s", rewritezone(s_to_c(p->s))); sep = 0; } else { dBputc(p->c); n++; sep = 1; } } n += dBprint("\r\n"); return n; } char * rewritezone(char *z) { int mindiff; char s; Tm *tm; static char x[7]; tm = localtime(time(0)); mindiff = tm->tzoff/60; /* if not in my timezone, don't change anything */ if(strcmp(tm->zone, z) != 0) return z; if(mindiff < 0){ s = '-'; mindiff = -mindiff; } else s = '+'; sprint(x, "%c%.2d%.2d", s, mindiff/60, mindiff%60); return x; } /* * stolen from libc/port/print.c */ #define SIZE 4096 int dBprint(char *fmt, ...) { char buf[SIZE], *out; va_list arg; int n; va_start(arg, fmt); out = vseprint(buf, buf+SIZE, fmt, arg); va_end(arg); if(debug){ Bwrite(&berr, buf, (long)(out-buf)); Bflush(&berr); } n = Bwrite(&bout, buf, (long)(out-buf)); Bflush(&bout); return n; } int dBputc(int x) { if(debug) Bputc(&berr, x); return Bputc(&bout, x); }