Subversion Repositories planix.SVN

Rev

Rev 2 | Blame | Compare with Previous | Last modification | View Log | RSS feed

#include "common.h"
#include "smtp.h"
#include <ctype.h>
#include <mp.h>
#include <libsec.h>
#include <auth.h>

static  char*   connect(char*);
static  char*   dotls(char*);
static  char*   doauth(char*);

void    addhostdom(String*, char*);
String* bangtoat(char*);
String* convertheader(String*);
int     dBprint(char*, ...);
int     dBputc(int);
char*   data(String*, Biobuf*);
char*   domainify(char*, char*);
String* fixrouteaddr(String*, Node*, Node*);
char*   getcrnl(String*);
int     getreply(void);
char*   hello(char*, int);
char*   mailfrom(char*);
int     printdate(Node*);
int     printheader(void);
void    putcrnl(char*, int);
void    quit(char*);
char*   rcptto(char*);
char    *rewritezone(char *);

#define Retry   "Retry, Temporary Failure"
#define Giveup  "Permanent Failure"

String  *reply;         /* last reply */
String  *toline;

int     alarmscale;
int     autistic;
int     debug;          /* true if we're debugging */
int     filter;
int     insecure;
int     last = 'n';     /* last character sent by putcrnl() */
int     ping;
int     quitting;       /* when error occurs in quit */
int     tryauth;        /* Try to authenticate, if supported */
int     trysecure;      /* Try to use TLS if the other side supports it */
int     okunksecure;    /* okay to use TLS to unknown servers */

char    *quitrv;        /* deferred return value when in quit */
char    ddomain[Maxdomain]; /* domain name of destination machine */
char    *gdomain;       /* domain name of gateway */
char    *uneaten;       /* first character after rfc822 headers */
char    *farend;        /* system we are trying to send to */
char    *user;          /* user we are authenticating as, if authenticating */
char    hostdomain[256];

Biobuf  bin;
Biobuf  bout;
Biobuf  berr;
Biobuf  bfile;

static int bustedmx;

void
usage(void)
{
        fprint(2, "usage: smtp [-aAdfips] [-b busted-mx] [-g gw] [-h host] "
                "[-u user] [.domain] net!host[!service] sender rcpt-list\n");
        exits(Giveup);
}

int
timeout(void *x, char *msg)
{
        USED(x);
        syslog(0, "smtp.fail", "interrupt: %s: %s", farend,  msg);
        if(strstr(msg, "alarm")){
                fprint(2, "smtp timeout: connection to %s timed out\n", farend);
                if(quitting)
                        exits(quitrv);
                exits(Retry);
        }
        if(strstr(msg, "closed pipe")){
                /* call _exits() to prevent Bio from trying to flush closed pipe */
                fprint(2, "smtp timeout: connection closed to %s\n", farend);
                if(quitting){
                        syslog(0, "smtp.fail", "closed pipe to %s", farend);
                        _exits(quitrv);
                }
                _exits(Retry);
        }
        return 0;
}

void
removenewline(char *p)
{
        int n = strlen(p)-1;

        if(n < 0)
                return;
        if(p[n] == '\n')
                p[n] = 0;
}

void
main(int argc, char **argv)
{
        int i, ok, rcvrs;
        char *addr, *rv, *trv, *host, *domain;
        char **errs;
        char hellodomain[256];
        String *from, *fromm, *sender;

        alarmscale = 60*1000;   /* minutes */
        quotefmtinstall();
        errs = malloc(argc*sizeof(char*));
        reply = s_new();
        host = 0;
        ARGBEGIN{
        case 'a':
                tryauth = 1;
                trysecure = 1;
                break;
        case 'A':       /* autistic: won't talk to us until we talk (Verizon) */
                autistic = 1;
                break;
        case 'b':
                if (bustedmx >= Maxbustedmx)
                        sysfatal("more than %d busted mxs given", Maxbustedmx);
                bustedmxs[bustedmx++] = EARGF(usage());
                break;
        case 'd':
                debug = 1;
                break;
        case 'f':
                filter = 1;
                break;
        case 'g':
                gdomain = EARGF(usage());
                break;
        case 'h':
                host = EARGF(usage());
                break;
        case 'i':
                insecure = 1;
                break;
        case 'o':
                okunksecure = 1;
                break;
        case 'p':
                alarmscale = 10*1000;   /* tens of seconds */
                ping = 1;
                break;
        case 's':
                trysecure = 1;
                break;
        case 'u':
                user = EARGF(usage());
                break;
        default:
                usage();
                break;
        }ARGEND;

        Binit(&berr, 2, OWRITE);
        Binit(&bfile, 0, OREAD);

        /*
         *  get domain and add to host name
         */
        if(*argv && **argv=='.') {
                domain = *argv;
                argv++; argc--;
        } else
                domain = domainname_read();
        if(domain == nil)
                fprint(2, "%s: nil domainname_read()\n", argv0);
        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 *
ckthumbs(TLSconn *c)
{
        Thumbprint *goodcerts;
        char *h, *err;
        uchar hash[SHA1dlen];

        err = nil;
        goodcerts = initThumbprints(smtpthumbs, smtpexclthumbs);
        if (goodcerts == nil) {
                if (!okunksecure)
                        syslog(0, "smtp", "bad thumbprints in %s", smtpthumbs);
                return Giveup;          /* how to recover? TLS is started */
        }

        /* compute sha1 hash of remote's certificate, see if we know it */
        sha1(c->cert, c->certlen, hash, nil);
        if (!okThumbprint(hash, goodcerts) && !okunksecure) {
                h = malloc(2*sizeof hash + 1);
                if (h != nil) {
                        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);
                }
                err = Giveup;           /* how to recover? TLS is started */
        }
        freeThumbprints(goodcerts);
        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)
{
        TLSconn *c;
        char *err;
        int fd;

        c = mallocz(sizeof(*c), 1);     /* Note: not freed on success */
        if (c == nil)
                return Giveup;

        dBprint("STARTTLS\r\n");
        if (getreply() != 2)
                return Giveup;

        fd = tlsClient(Bfildes(&bout), c);
        if (fd < 0) {
                syslog(0, "smtp", "tlsClient to %q: %r", ddomain);
                return Giveup;
        }

        err = ckthumbs(c);
        if (err && !okunksecure) {
                free(c);
                close(fd);
                return err;             /* how to recover? TLS is started */
        }

        Bterm(&bin);
        Bterm(&bout);

        /*
         * set up bin & bout to use the TLS fd, i/o upon which generates
         * i/o on the original, underlying fd.
         */
        Binit(&bin, fd, OREAD);
        fd = dup(fd, -1);
        Binit(&bout, fd, OWRITE);

        syslog(0, "smtp", "started TLS to %q", ddomain);
        return(hello(me, 1));
}

static char *
doauth(char *methods)
{
        char *buf, *base64;
        int n;
        DS ds;
        UserPasswd *p;

        dial_string_parse(ddomain, &ds);

        if(user != nil)
                p = auth_getuserpasswd(nil,
                  "proto=pass service=smtp server=%q user=%q", ds.host, user);
        else
                p = auth_getuserpasswd(nil,
                  "proto=pass service=smtp server=%q", ds.host);
        if (p == nil)
                return Giveup;

        if (strstr(methods, "LOGIN")){
                dBprint("AUTH LOGIN\r\n");
                if (getreply() != 3)
                        return Retry;

                n = strlen(p->user);
                base64 = malloc(2*n);
                if (base64 == nil)
                        return Retry;   /* Out of memory */
                enc64(base64, 2*n, (uchar *)p->user, n);
                dBprint("%s\r\n", base64);
                if (getreply() != 3)
                        return Retry;

                n = strlen(p->passwd);
                base64 = malloc(2*n);
                if (base64 == nil)
                        return Retry;   /* Out of memory */
                enc64(base64, 2*n, (uchar *)p->passwd, n);
                dBprint("%s\r\n", base64);
                if (getreply() != 2)
                        return Retry;

                free(base64);
        }
        else
        if (strstr(methods, "PLAIN")){
                n = strlen(p->user) + strlen(p->passwd) + 3;
                buf = malloc(n);
                base64 = malloc(2 * n);
                if (buf == nil || base64 == nil) {
                        free(buf);
                        return Retry;   /* Out of memory */
                }
                snprint(buf, n, "%c%s%c%s", 0, p->user, 0, p->passwd);
                enc64(base64, 2 * n, (uchar *)buf, n - 1);
                free(buf);
                dBprint("AUTH PLAIN %s\r\n", base64);
                free(base64);
                if (getreply() != 2)
                        return Retry;
        }
        else
                return "No supported AUTH method";
        return(0);
}

char *
hello(char *me, int encrypted)
{
        int ehlo;
        String *r;
        char *ret, *s, *t;

        if (!encrypted) {
                /*
                 * Verizon fails to print the smtp greeting banner when it
                 * answers a call.  Send a no-op in the hope of making it
                 * talk.
                 */
                if (autistic) {
                        dBprint("NOOP\r\n");
                        getreply();     /* consume the smtp greeting */
                        /* next reply will be response to noop */
                }
                switch(getreply()){
                case 2:
                        break;
                case 5:
                        return Giveup;
                default:
                        return Retry;
                }
        }

        ehlo = 1;
  Again:
        if(ehlo)
                dBprint("EHLO %s\r\n", me);
        else
                dBprint("HELO %s\r\n", me);
        switch (getreply()) {
        case 2:
                break;
        case 5:
                if(ehlo){
                        ehlo = 0;
                        goto Again;
                }
                return Giveup;
        default:
                return Retry;
        }
        r = s_clone(reply);
        if(r == nil)
                return Retry;   /* Out of memory or couldn't get string */

        /* Invariant: every line has a newline, a result of getcrlf() */
        for(s = s_to_c(r); (t = strchr(s, '\n')) != nil; s = t + 1){
                *t = '\0';
                for (t = s; *t != '\0'; t++)
                        *t = toupper(*t);
                if(!encrypted && trysecure &&
                    (strcmp(s, "250-STARTTLS") == 0 ||
                     strcmp(s, "250 STARTTLS") == 0)){
                        s_free(r);
                        return dotls(me);
                }
                if(tryauth && (encrypted || insecure) &&
                    (strncmp(s, "250 AUTH", strlen("250 AUTH")) == 0 ||
                     strncmp(s, "250-AUTH", strlen("250 AUTH")) == 0)){
                        ret = doauth(s + strlen("250 AUTH "));
                        s_free(r);
                        return ret;
                }
        }
        s_free(r);
        return 0;
}

/*
 *  report sender to remote
 */
char *
mailfrom(char *from)
{
        if(!returnable(from))
                dBprint("MAIL FROM:<>\r\n");
        else
        if(strchr(from, '@'))
                dBprint("MAIL FROM:<%s>\r\n", from);
        else
                dBprint("MAIL FROM:<%s@%s>\r\n", from, hostdomain);
        switch(getreply()){
        case 2:
                break;
        case 5:
                return Giveup;
        default:
                return Retry;
        }
        return 0;
}

/*
 *  report a recipient to remote
 */
char *
rcptto(char *to)
{
        String *s;

        s = unescapespecial(bangtoat(to));
        if(toline == 0)
                toline = s_new();
        else
                s_append(toline, ", ");
        s_append(toline, s_to_c(s));
        if(strchr(s_to_c(s), '@'))
                dBprint("RCPT TO:<%s>\r\n", s_to_c(s));
        else {
                s_append(toline, "@");
                s_append(toline, ddomain);
                dBprint("RCPT TO:<%s@%s>\r\n", s_to_c(s), ddomain);
        }
        alarm(10*alarmscale);
        switch(getreply()){
        case 2:
                break;
        case 5:
                return Giveup;
        default:
                return Retry;
        }
        return 0;
}

static char hex[] = "0123456789abcdef";

/*
 *  send the damn thing
 */
char *
data(String *from, Biobuf *b)
{
        char *buf, *cp;
        int i, n, nbytes, bufsize, eof, r;
        String *fromline;
        char errmsg[Errlen];
        char id[40];

        /*
         *  input the header.
         */

        buf = malloc(1);
        if(buf == 0){
                s_append(s_restart(reply), "out of memory");
                return Retry;
        }
        n = 0;
        eof = 0;
        for(;;){
                cp = Brdline(b, '\n');
                if(cp == nil){
                        eof = 1;
                        break;
                }
                nbytes = Blinelen(b);
                buf = realloc(buf, n+nbytes+1);
                if(buf == 0){
                        s_append(s_restart(reply), "out of memory");
                        return Retry;
                }
                strncpy(buf+n, cp, nbytes);
                n += nbytes;
                if(nbytes == 1)         /* end of header */
                        break;
        }
        buf[n] = 0;
        bufsize = n;

        /*
         *  parse the header, turn all addresses into @ format
         */
        yyinit(buf, n);
        yyparse();

        /*
         *  print message observing '.' escapes and using \r\n for \n
         */
        alarm(20*alarmscale);
        if(!filter){
                dBprint("DATA\r\n");
                switch(getreply()){
                case 3:
                        break;
                case 5:
                        free(buf);
                        return Giveup;
                default:
                        free(buf);
                        return Retry;
                }
        }
        /*
         *  send header.  add a message-id, a sender, and a date if there
         *  isn't one
         */
        nbytes = 0;
        fromline = convertheader(from);
        uneaten = buf;

        srand(truerand());
        if(messageid == 0){
                for(i=0; i<16; i++){
                        r = rand()&0xFF;
                        id[2*i] = hex[r&0xF];
                        id[2*i+1] = hex[(r>>4)&0xF];
                }
                id[2*i] = '\0';
                nbytes += Bprint(&bout, "Message-ID: <%s@%s>\r\n", id, hostdomain);
                if(debug)
                        Bprint(&berr, "Message-ID: <%s@%s>\r\n", id, hostdomain);
        }

        if(originator==0){
                nbytes += Bprint(&bout, "From: %s\r\n", s_to_c(fromline));
                if(debug)
                        Bprint(&berr, "From: %s\r\n", s_to_c(fromline));
        }
        s_free(fromline);

        if(destination == 0 && toline)
                if(*s_to_c(toline) == '@'){     /* route addr */
                        nbytes += Bprint(&bout, "To: <%s>\r\n", s_to_c(toline));
                        if(debug)
                                Bprint(&berr, "To: <%s>\r\n", s_to_c(toline));
                } else {
                        nbytes += Bprint(&bout, "To: %s\r\n", s_to_c(toline));
                        if(debug)
                                Bprint(&berr, "To: %s\r\n", s_to_c(toline));
                }

        if(date==0 && udate)
                nbytes += printdate(udate);
        if (usys)
                uneaten = usys->end + 1;
        nbytes += printheader();
        if (*uneaten != '\n')
                putcrnl("\n", 1);

        /*
         *  send body
         */

        putcrnl(uneaten, buf+n - uneaten);
        nbytes += buf+n - uneaten;
        if(eof == 0){
                for(;;){
                        n = Bread(b, buf, bufsize);
                        if(n < 0){
                                rerrstr(errmsg, sizeof(errmsg));
                                s_append(s_restart(reply), errmsg);
                                free(buf);
                                return Retry;
                        }
                        if(n == 0)
                                break;
                        alarm(10*alarmscale);
                        putcrnl(buf, n);
                        nbytes += n;
                }
        }
        free(buf);
        if(!filter){
                if(last != '\n')
                        dBprint("\r\n.\r\n");
                else
                        dBprint(".\r\n");
                alarm(10*alarmscale);
                switch(getreply()){
                case 2:
                        break;
                case 5:
                        return Giveup;
                default:
                        return Retry;
                }
                syslog(0, "smtp", "%s sent %d bytes to %s", s_to_c(from),
                                nbytes, s_to_c(toline));/**/
        }
        return 0;
}

/*
 *  we're leaving
 */
void
quit(char *rv)
{
                /* 60 minutes to quit */
        quitting = 1;
        quitrv = rv;
        alarm(60*alarmscale);
        dBprint("QUIT\r\n");
        getreply();
        Bterm(&bout);
        Bterm(&bfile);
}

/*
 *  read a reply into a string, return the reply code
 */
int
getreply(void)
{
        char *line;
        int rv;

        reply = s_reset(reply);
        for(;;){
                line = getcrnl(reply);
                if(debug)
                        Bflush(&berr);
                if(line == 0)
                        return -1;
                if(!isdigit(line[0]) || !isdigit(line[1]) || !isdigit(line[2]))
                        return -1;
                if(line[3] != '-')
                        break;
        }
        if(debug)
                Bflush(&berr);
        rv = atoi(line)/100;
        return rv;
}
void
addhostdom(String *buf, char *host)
{
        s_append(buf, "@");
        s_append(buf, host);
}

/*
 *      Convert from `bang' to `source routing' format.
 *
 *         a.x.y!b.p.o!c!d ->   @a.x.y:c!d@b.p.o
 */
String *
bangtoat(char *addr)
{
        String *buf;
        register int i;
        int j, d;
        char *field[128];

        /* parse the '!' format address */
        buf = s_new();
        for(i = 0; addr; i++){
                field[i] = addr;
                addr = strchr(addr, '!');
                if(addr)
                        *addr++ = 0;
        }
        if (i==1) {
                s_append(buf, field[0]);
                return buf;
        }

        /*
         *  count leading domain fields (non-domains don't count)
         */
        for(d = 0; d<i-1; d++)
                if(strchr(field[d], '.')==0)
                        break;
        /*
         *  if there are more than 1 leading domain elements,
         *  put them in as source routing
         */
        if(d > 1){
                addhostdom(buf, field[0]);
                for(j=1; j<d-1; j++){
                        s_append(buf, ",");
                        s_append(buf, "@");
                        s_append(buf, field[j]);
                }
                s_append(buf, ":");
        }

        /*
         *  throw in the non-domain elements separated by '!'s
         */
        s_append(buf, field[d]);
        for(j=d+1; j<=i-1; j++) {
                s_append(buf, "!");
                s_append(buf, field[j]);
        }
        if(d)
                addhostdom(buf, field[d-1]);
        return buf;
}

/*
 *  convert header addresses to @ format.
 *  if the address is a source address, and a domain is specified,
 *  make sure it falls in the domain.
 */
String*
convertheader(String *from)
{
        Field *f;
        Node *p, *lastp;
        String *a;

        if(!returnable(s_to_c(from))){
                from = s_new();
                s_append(from, "Postmaster");
                addhostdom(from, hostdomain);
        } else
        if(strchr(s_to_c(from), '@') == 0){
                a = username(from);
                if(a) {
                        s_append(a, " <");
                        s_append(a, s_to_c(from));
                        addhostdom(a, hostdomain);
                        s_append(a, ">");
                        from = a;
                } else {
                        from = s_copy(s_to_c(from));
                        addhostdom(from, hostdomain);
                }
        } else
                from = s_copy(s_to_c(from));
        for(f = firstfield; f; f = f->next){
                lastp = 0;
                for(p = f->node; p; lastp = p, p = p->next){
                        if(!p->addr)
                                continue;
                        a = bangtoat(s_to_c(p->s));
                        s_free(p->s);
                        if(strchr(s_to_c(a), '@') == 0)
                                addhostdom(a, hostdomain);
                        else if(*s_to_c(a) == '@')
                                a = fixrouteaddr(a, p->next, lastp);
                        p->s = a;
                }
        }
        return from;
}
/*
 *      ensure route addr has brackets around it
 */
String*
fixrouteaddr(String *raddr, Node *next, Node *last)
{
        String *a;

        if(last && last->c == '<' && next && next->c == '>')
                return raddr;                   /* properly formed already */

        a = s_new();
        s_append(a, "<");
        s_append(a, s_to_c(raddr));
        s_append(a, ">");
        s_free(raddr);
        return a;
}

/*
 *  print out the parsed header
 */
int
printheader(void)
{
        int n, len;
        Field *f;
        Node *p;
        char *cp;
        char c[1];

        n = 0;
        for(f = firstfield; f; f = f->next){
                for(p = f->node; p; p = p->next){
                        if(p->s)
                                n += dBprint("%s", s_to_c(p->s));
                        else {
                                c[0] = p->c;
                                putcrnl(c, 1);
                                n++;
                        }
                        if(p->white){
                                cp = s_to_c(p->white);
                                len = strlen(cp);
                                putcrnl(cp, len);
                                n += len;
                        }
                        uneaten = p->end;
                }
                putcrnl("\n", 1);
                n++;
                uneaten++;              /* skip newline */
        }
        return n;
}

/*
 *  add a domain onto an name, return the new name
 */
char *
domainify(char *name, char *domain)
{
        static String *s;
        char *p;

        if(domain==0 || strchr(name, '.')!=0)
                return name;

        s = s_reset(s);
        s_append(s, name);
        p = strchr(domain, '.');
        if(p == 0){
                s_append(s, ".");
                p = domain;
        }
        s_append(s, p);
        return s_to_c(s);
}

/*
 *  print message observing '.' escapes and using \r\n for \n
 */
void
putcrnl(char *cp, int n)
{
        int c;

        for(; n; n--, cp++){
                c = *cp;
                if(c == '\n')
                        dBputc('\r');
                else if(c == '.' && last=='\n')
                        dBputc('.');
                dBputc(c);
                last = c;
        }
}

/*
 *  Get a line including a crnl into a string.  Convert crnl into nl.
 */
char *
getcrnl(String *s)
{
        int c;
        int count;

        count = 0;
        for(;;){
                c = Bgetc(&bin);
                if(debug)
                        Bputc(&berr, c);
                switch(c){
                case -1:
                        s_append(s, "connection closed unexpectedly by remote system");
                        s_terminate(s);
                        return 0;
                case '\r':
                        c = Bgetc(&bin);
                        if(c == '\n'){
                case '\n':
                                s_putc(s, c);
                                if(debug)
                                        Bputc(&berr, c);
                                count++;
                                s_terminate(s);
                                return s->ptr - count;
                        }
                        Bungetc(&bin);
                        s_putc(s, '\r');
                        if(debug)
                                Bputc(&berr, '\r');
                        count++;
                        break;
                default:
                        s_putc(s, c);
                        count++;
                        break;
                }
        }
}

/*
 *  print out a parsed date
 */
int
printdate(Node *p)
{
        int n, sep = 0;

        n = dBprint("Date: %s,", s_to_c(p->s));
        for(p = p->next; p; p = p->next){
                if(p->s){
                        if(sep == 0) {
                                dBputc(' ');
                                n++;
                        }
                        if (p->next)
                                n += dBprint("%s", s_to_c(p->s));
                        else
                                n += dBprint("%s", rewritezone(s_to_c(p->s)));
                        sep = 0;
                } else {
                        dBputc(p->c);
                        n++;
                        sep = 1;
                }
        }
        n += dBprint("\r\n");
        return n;
}

char *
rewritezone(char *z)
{
        int mindiff;
        char s;
        Tm *tm;
        static char x[7];

        tm = localtime(time(0));
        mindiff = tm->tzoff/60;

        /* if not in my timezone, don't change anything */
        if(strcmp(tm->zone, z) != 0)
                return z;

        if(mindiff < 0){
                s = '-';
                mindiff = -mindiff;
        } else
                s = '+';

        sprint(x, "%c%.2d%.2d", s, mindiff/60, mindiff%60);
        return x;
}

/*
 *  stolen from libc/port/print.c
 */
#define SIZE    4096
int
dBprint(char *fmt, ...)
{
        char buf[SIZE], *out;
        va_list arg;
        int n;

        va_start(arg, fmt);
        out = vseprint(buf, buf+SIZE, fmt, arg);
        va_end(arg);
        if(debug){
                Bwrite(&berr, buf, (long)(out-buf));
                Bflush(&berr);
        }
        n = Bwrite(&bout, buf, (long)(out-buf));
        Bflush(&bout);
        return n;
}

int
dBputc(int x)
{
        if(debug)
                Bputc(&berr, x);
        return Bputc(&bout, x);
}