tThanks to John Cummings. - plan9port - [fork] Plan 9 from user space
git clone git://src.adamsgaard.dk/plan9port
Log
Files
Refs
README
LICENSE
---
commit 5cdb17983ae6e6367ad7a940cb219eab247a9304
parent cd3745196389579fb78b9b01ef1daefb5a57aa71
Author: rsc 
Date:   Sat, 29 Oct 2005 16:26:44 +0000

Thanks to John Cummings.

Diffstat:
  A src/cmd/upas/common/libsys.c        |    1001 +++++++++++++++++++++++++++++++
  A src/cmd/upas/common/mail.c          |      57 +++++++++++++++++++++++++++++++
  A src/cmd/upas/common/makefile        |      18 ++++++++++++++++++
  A src/cmd/upas/common/mkfile          |      20 ++++++++++++++++++++
  A src/cmd/upas/common/process.c       |     175 +++++++++++++++++++++++++++++++
  A src/cmd/upas/common/sys.h           |      85 +++++++++++++++++++++++++++++++
  A src/cmd/upas/filterkit/dat.h        |       8 ++++++++
  A src/cmd/upas/filterkit/deliver.c    |      60 +++++++++++++++++++++++++++++++
  A src/cmd/upas/filterkit/list.c       |     315 +++++++++++++++++++++++++++++++
  A src/cmd/upas/filterkit/mkfile       |      21 +++++++++++++++++++++
  A src/cmd/upas/filterkit/pipefrom.sa… |      24 ++++++++++++++++++++++++
  A src/cmd/upas/filterkit/pipeto.samp… |      73 +++++++++++++++++++++++++++++++
  A src/cmd/upas/filterkit/pipeto.samp… |      43 ++++++++++++++++++++++++++++++
  A src/cmd/upas/filterkit/readaddrs.c  |      98 +++++++++++++++++++++++++++++++
  A src/cmd/upas/filterkit/token.c      |      89 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/dat.h               |     221 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/fs.c                |    1704 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/imap4.c             |     876 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/mbox.c              |    1601 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/mkfile              |      29 +++++++++++++++++++++++++++++
  A src/cmd/upas/fs/mkfile.9            |      27 +++++++++++++++++++++++++++
  A src/cmd/upas/fs/plan9.c             |     405 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/pop3.c              |     700 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/readdir.c           |      15 +++++++++++++++
  A src/cmd/upas/fs/strtotm.c           |     113 +++++++++++++++++++++++++++++++
  A src/cmd/upas/fs/tester.c            |      81 ++++++++++++++++++++++++++++++
  A src/cmd/upas/marshal/marshal.c      |    1857 ++++++++++++++++++++++++++++++
  A src/cmd/upas/marshal/mkfile         |      20 ++++++++++++++++++++
  A src/cmd/upas/misc/gone.fishing      |       9 +++++++++
  A src/cmd/upas/misc/gone.msg          |       4 ++++
  A src/cmd/upas/misc/mail.c            |      51 +++++++++++++++++++++++++++++++
  A src/cmd/upas/misc/mail.rc           |      12 ++++++++++++
  A src/cmd/upas/misc/mail.sh           |      12 ++++++++++++
  A src/cmd/upas/misc/makefile          |      44 +++++++++++++++++++++++++++++++
  A src/cmd/upas/misc/mkfile            |      39 +++++++++++++++++++++++++++++++
  A src/cmd/upas/misc/namefiles         |       2 ++
  A src/cmd/upas/misc/omail.rc          |      14 ++++++++++++++
  A src/cmd/upas/misc/qmail             |       6 ++++++
  A src/cmd/upas/misc/remotemail        |       7 +++++++
  A src/cmd/upas/misc/rewrite           |      20 ++++++++++++++++++++
  A src/cmd/upas/ml/common.c            |     197 +++++++++++++++++++++++++++++++
  A src/cmd/upas/ml/dat.h               |      25 +++++++++++++++++++++++++
  A src/cmd/upas/ml/mkfile              |      40 +++++++++++++++++++++++++++++++
  A src/cmd/upas/ml/ml.c                |     167 +++++++++++++++++++++++++++++++
  A src/cmd/upas/ml/mlmgr.c             |     110 +++++++++++++++++++++++++++++++
  A src/cmd/upas/ml/mlowner.c           |      64 +++++++++++++++++++++++++++++++
  A src/cmd/upas/ned/mkfile             |      20 ++++++++++++++++++++
  A src/cmd/upas/ned/nedmail.c          |    2586 +++++++++++++++++++++++++++++++
  A src/cmd/upas/pop3/mkfile            |      16 ++++++++++++++++
  A src/cmd/upas/pop3/pop3.c            |     804 ++++++++++++++++++++++++++++++
  A src/cmd/upas/q/mkfile               |      22 ++++++++++++++++++++++
  A src/cmd/upas/q/qer.c                |     193 +++++++++++++++++++++++++++++++
  A src/cmd/upas/q/runq.c               |     766 +++++++++++++++++++++++++++++++
  A src/cmd/upas/scanmail/common.c      |     667 +++++++++++++++++++++++++++++++
  A src/cmd/upas/scanmail/mkfile        |      24 ++++++++++++++++++++++++
  A src/cmd/upas/scanmail/scanmail.c    |     476 +++++++++++++++++++++++++++++++
  A src/cmd/upas/scanmail/spam.h        |      62 +++++++++++++++++++++++++++++++
  A src/cmd/upas/scanmail/testscan.c    |     212 ++++++++++++++++++++++++++++++
  A src/cmd/upas/send/authorize.c       |      29 +++++++++++++++++++++++++++++
  A src/cmd/upas/send/bind.c            |     133 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/cat_mail.c        |      60 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/dest.c            |     260 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/filter.c          |     128 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/gateway.c         |      24 ++++++++++++++++++++++++
  A src/cmd/upas/send/local.c           |     129 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/log.c             |      85 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/main.c            |     575 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/makefile          |      46 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/message.c         |     573 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/mkfile            |      52 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/regtest.c         |      36 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/rewrite.c         |     315 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/send.h            |     108 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/skipequiv.c       |      93 +++++++++++++++++++++++++++++++
  A src/cmd/upas/send/translate.c       |      43 ++++++++++++++++++++++++++++++
  A src/cmd/upas/send/tryit             |      29 +++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/greylist.c        |     274 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/mkfile            |      54 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/mxdial.c          |     333 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/rfc822.tab.c      |    1260 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/rfc822.tab.h      |      98 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/rfc822.y          |     778 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/rmtdns.c          |      58 ++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/smtp.c            |    1122 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/smtp.h            |      61 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/smtpd.c           |    1494 ++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/smtpd.h           |      68 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/smtpd.y           |     317 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/spam.c            |     591 +++++++++++++++++++++++++++++++
  A src/cmd/upas/smtp/y.tab.h           |      25 +++++++++++++++++++++++++
  A src/cmd/upas/unesc/mkfile           |      17 +++++++++++++++++
  A src/cmd/upas/unesc/unesc.c          |      48 +++++++++++++++++++++++++++++++
  A src/cmd/upas/vf/mkfile              |      20 ++++++++++++++++++++
  A src/cmd/upas/vf/vf.c                |    1110 +++++++++++++++++++++++++++++++

94 files changed, 26853 insertions(+), 0 deletions(-)
---
diff --git a/src/cmd/upas/common/libsys.c b/src/cmd/upas/common/libsys.c
t@@ -0,0 +1,1001 @@
+#include "common.h"
+#include 
+#include 
+
+/*
+ *  number of predefined fd's
+ */
+int nsysfile=3;
+
+static char err[Errlen];
+
+/*
+ *  return the date
+ */
+extern char *
+thedate(void)
+{
+        static char now[64];
+        char *cp;
+
+        strcpy(now, ctime(time(0)));
+        cp = strchr(now, '\n');
+        if(cp)
+                *cp = 0;
+        return now;
+}
+
+/*
+ *  return the user id of the current user
+ */
+extern char *
+getlog(void)
+{
+        return getuser();
+}
+#if 0  /* jpc */
+extern char *
+getlog(void)
+{
+        static char user[64];
+        int fd;
+        int n;
+
+        fd = open("/dev/user", 0);
+        if(fd < 0)
+                return nil;
+        if((n=read(fd, user, sizeof(user)-1)) <= 0)
+                return nil;
+        close(fd);
+        user[n] = 0;
+        return user;
+}
+#endif /* jpc */
+/*
+ *  return the lock name (we use one lock per directory)
+ */
+static String *
+lockname(char *path)
+{
+        String *lp;
+        char *cp;
+
+        /*
+         *  get the name of the lock file
+         */
+        lp = s_new();
+        cp = strrchr(path, '/');
+        if(cp)
+                s_nappend(lp, path, cp - path + 1);
+        s_append(lp, "L.mbox");
+
+        return lp;
+}
+
+int
+syscreatelocked(char *path, int mode, int perm)
+{
+        return create(path, mode, DMEXCL|perm);
+}
+
+int
+sysopenlocked(char *path, int mode)
+{
+/*        return open(path, OEXCL|mode);/**/
+        return open(path, mode);                /* until system call is fixed */
+}
+
+int
+sysunlockfile(int fd)
+{
+        return close(fd);
+}
+
+/*
+ *  try opening a lock file.  If it doesn't exist try creating it.
+ */
+static int
+openlockfile(Mlock *l)
+{
+        int fd;
+        Dir *d;
+        Dir nd;
+        char *p;
+
+        fd = open(s_to_c(l->name), OREAD);
+        if(fd >= 0){
+                l->fd = fd;
+                return 0;
+        }
+
+        d = dirstat(s_to_c(l->name));
+        if(d == nil){
+                /* file doesn't exist */
+                /* try creating it */
+                fd = create(s_to_c(l->name), OREAD, DMEXCL|0666);
+                if(fd >= 0){
+                        nulldir(&nd);
+                        nd.mode = DMEXCL|0666;
+                        if(dirfwstat(fd, &nd) < 0){
+                                /* if we can't chmod, don't bother */
+                                /* live without the lock but log it */
+                                syslog(0, "mail", "lock error: %s: %r", s_to_c(l->name));
+                                remove(s_to_c(l->name));
+                        }
+                        l->fd = fd;
+                        return 0;
+                }
+
+                /* couldn't create */
+                /* do we have write access to the directory? */
+                p = strrchr(s_to_c(l->name), '/');
+                if(p != 0){
+                        *p = 0;
+                        fd = access(s_to_c(l->name), 2);
+                        *p = '/';
+                        if(fd < 0){
+                                /* live without the lock but log it */
+                                syslog(0, "mail", "lock error: %s: %r", s_to_c(l->name));
+                                return 0;
+                        }
+                } else {
+                        fd = access(".", 2);
+                        if(fd < 0){
+                                /* live without the lock but log it */
+                                syslog(0, "mail", "lock error: %s: %r", s_to_c(l->name));
+                                return 0;
+                        }
+                }
+        } else
+                free(d);
+
+        return 1; /* try again later */
+}
+
+#define LSECS 5*60
+
+/*
+ *  Set a lock for a particular file.  The lock is a file in the same directory
+ *  and has L. prepended to the name of the last element of the file name.
+ */
+extern Mlock *
+syslock(char *path)
+{
+        Mlock *l;
+        int tries;
+
+        l = mallocz(sizeof(Mlock), 1);
+        if(l == 0)
+                return nil;
+
+        l->name = lockname(path);
+
+        /*
+         *  wait LSECS seconds for it to unlock
+         */
+        for(tries = 0; tries < LSECS*2; tries++){
+                switch(openlockfile(l)){
+                case 0:
+                        return l;
+                case 1:
+                        sleep(500);
+                        break;
+                default:
+                        goto noway;
+                }
+        }
+
+noway:
+        s_free(l->name);
+        free(l);
+        return nil;
+}
+
+/*
+ *  like lock except don't wait
+ */
+extern Mlock *
+trylock(char *path)
+{
+        Mlock *l;
+        char buf[1];
+        int fd;
+
+        l = malloc(sizeof(Mlock));
+        if(l == 0)
+                return 0;
+
+        l->name = lockname(path);
+        if(openlockfile(l) != 0){
+                s_free(l->name);
+                free(l);
+                return 0;
+        }
+        
+        /* fork process to keep lock alive */
+        switch(l->pid = rfork(RFPROC)){
+        default:
+                break;
+        case 0:
+                fd = l->fd;
+                for(;;){
+                        sleep(1000*60);
+                        if(pread(fd, buf, 1, 0) < 0)
+                                break;
+                }
+                _exits(0);
+        }
+        return l;
+}
+
+extern void
+syslockrefresh(Mlock *l)
+{
+        char buf[1];
+
+        pread(l->fd, buf, 1, 0);
+}
+
+extern void
+sysunlock(Mlock *l)
+{
+        if(l == 0)
+                return;
+        if(l->name){
+                s_free(l->name);
+        }
+        if(l->fd >= 0)
+                close(l->fd);
+        if(l->pid > 0)
+                postnote(PNPROC, l->pid, "time to die");
+        free(l);
+}
+
+/*
+ *  Open a file.  The modes are:
+ *
+ *        l        - locked
+ *        a        - set append permissions
+ *        r        - readable
+ *        w        - writable
+ *        A        - append only (doesn't exist in Bio)
+ */
+extern Biobuf *
+sysopen(char *path, char *mode, ulong perm)
+{
+        int sysperm;
+        int sysmode;
+        int fd;
+        int docreate;
+        int append;
+        int truncate;
+        Dir *d, nd;
+        Biobuf *bp;
+
+        /*
+         *  decode the request
+         */
+        sysperm = 0;
+        sysmode = -1;
+        docreate = 0;
+        append = 0;
+        truncate = 0;
+         for(; mode && *mode; mode++)
+                switch(*mode){
+                case 'A':
+                        sysmode = OWRITE;
+                        append = 1;
+                        break;
+                case 'c':
+                        docreate = 1;
+                        break;
+                case 'l':
+                        sysperm |= DMEXCL;
+                        break;
+                case 'a':
+                        sysperm |= DMAPPEND;
+                        break;
+                case 'w':
+                        if(sysmode == -1)
+                                sysmode = OWRITE;
+                        else
+                                sysmode = ORDWR;
+                        break;
+                case 'r':
+                        if(sysmode == -1)
+                                sysmode = OREAD;
+                        else
+                                sysmode = ORDWR;
+                        break;
+                case 't':
+                        truncate = 1;
+                        break;
+                default:
+                        break;
+                }
+        switch(sysmode){
+        case OREAD:
+        case OWRITE:
+        case ORDWR:
+                break;
+        default:
+                if(sysperm&DMAPPEND)
+                        sysmode = OWRITE;
+                else
+                        sysmode = OREAD;
+                break;
+        }
+
+        /*
+         *  create file if we need to
+         */
+        if(truncate)
+                sysmode |= OTRUNC;
+        fd = open(path, sysmode);
+        if(fd < 0){
+                d = dirstat(path);
+                if(d == nil){
+                        if(docreate == 0)
+                                return 0;
+
+                        fd = create(path, sysmode, sysperm|perm);
+                        if(fd < 0)
+                                return 0;
+                        nulldir(&nd);
+                        nd.mode = sysperm|perm;
+                        dirfwstat(fd, &nd);
+                } else {
+                        free(d);
+                        return 0;
+                }
+        }
+
+        bp = (Biobuf*)malloc(sizeof(Biobuf));
+        if(bp == 0){
+                close(fd);
+                return 0;
+        }
+        memset(bp, 0, sizeof(Biobuf));
+        Binit(bp, fd, sysmode&~OTRUNC);
+
+        if(append)
+                Bseek(bp, 0, 2);
+        return bp;
+}
+
+/*
+ *  close the file, etc.
+ */
+int
+sysclose(Biobuf *bp)
+{
+        int rv;
+
+        rv = Bterm(bp);
+        close(Bfildes(bp));
+        free(bp);
+        return rv;
+}
+
+/*
+ *  create a file
+ */
+int
+syscreate(char *file, int mode, ulong perm)
+{
+        return create(file, mode, perm);
+}
+
+/*
+ *  make a directory
+ */
+int
+sysmkdir(char *file, ulong perm)
+{
+        int fd;
+
+        if((fd = create(file, OREAD, DMDIR|perm)) < 0)
+                return -1;
+        close(fd);
+        return 0;
+}
+
+/*
+ *  change the group of a file
+ */
+int
+syschgrp(char *file, char *group)
+{
+        Dir nd;
+
+        if(group == 0)
+                return -1;
+        nulldir(&nd);
+        nd.gid = group;
+        return dirwstat(file, &nd);
+}
+
+extern int
+sysdirreadall(int fd, Dir **d)
+{
+        return dirreadall(fd, d);
+}
+
+/*
+ *  read in the system name
+ */
+extern char *
+sysname_read(void)
+{
+        static char name[128];
+        char *cp;
+
+        cp = getenv("site");
+        if(cp == 0 || *cp == 0)
+                cp = alt_sysname_read();
+        if(cp == 0 || *cp == 0)
+                cp = "kremvax";
+        strecpy(name, name+sizeof name, cp);
+        return name;
+}
+extern char *
+alt_sysname_read(void)
+{
+        static char name[128];
+        int n, fd;
+
+        fd = open("/dev/sysname", OREAD);
+        if(fd < 0)
+                return 0;
+        n = read(fd, name, sizeof(name)-1);
+        close(fd);
+        if(n <= 0)
+                return 0;
+        name[n] = 0;
+        return name;
+}
+
+/*
+ *  get all names
+ */
+extern char**
+sysnames_read(void)
+{
+        static char **namev;
+        Ndbtuple *t, *nt;
+        Ndb* db;
+        Ndbs s;
+        int n;
+        char *cp;
+
+        if(namev)
+                return namev;
+
+        /* free(csgetvalue(0, "sys", alt_sysname_read(), "dom", &t));  jpc */
+        db = ndbopen(unsharp("#9/ndb/local"));
+        free(ndbgetvalue(db, &s, "sys", sysname(),"dom", &t));
+        /* t = nil; /* jpc */
+        /* fprint(2,"csgetvalue called: fixme"); /* jpc */
+
+        n = 0;
+        for(nt = t; nt; nt = nt->entry)
+                if(strcmp(nt->attr, "dom") == 0)
+                        n++;
+
+        namev = (char**)malloc(sizeof(char *)*(n+3));
+
+        if(namev){
+                n = 0;
+                namev[n++] = strdup(sysname_read());
+                cp = alt_sysname_read();
+                if(cp)
+                        namev[n++] = strdup(cp);
+                for(nt = t; nt; nt = nt->entry)
+                        if(strcmp(nt->attr, "dom") == 0)
+                                namev[n++] = strdup(nt->val);
+                namev[n] = 0;
+        }
+        if(t)
+                ndbfree(t);
+
+        return namev;
+}
+
+/*
+ *  read in the domain name
+ */
+extern char *
+domainname_read(void)
+{
+        char **namev;
+
+        for(namev = sysnames_read(); *namev; namev++)
+                if(strchr(*namev, '.'))
+                        return *namev;
+        return 0;
+}
+
+/*
+ *  return true if the last error message meant file
+ *  did not exist.
+ */
+extern int
+e_nonexistent(void)
+{
+        rerrstr(err, sizeof(err));
+        return strcmp(err, "file does not exist") == 0;
+}
+
+/*
+ *  return true if the last error message meant file
+ *  was locked.
+ */
+extern int
+e_locked(void)
+{
+        rerrstr(err, sizeof(err));
+        return strcmp(err, "open/create -- file is locked") == 0;
+}
+
+/*
+ *  return the length of a file
+ */
+extern long
+sysfilelen(Biobuf *fp)
+{
+        Dir *d;
+        long rv;
+
+        d = dirfstat(Bfildes(fp));
+        if(d == nil)
+                return -1;
+        rv = d->length;
+        free(d);
+        return rv;
+}
+
+/*
+ *  remove a file
+ */
+extern int
+sysremove(char *path)
+{
+        return remove(path);
+}
+
+/*
+ *  rename a file, fails unless both are in the same directory
+ */
+extern int
+sysrename(char *old, char *new)
+{
+        Dir d;
+        char *obase;
+        char *nbase;
+
+        obase = strrchr(old, '/');
+        nbase = strrchr(new, '/');
+        if(obase){
+                if(nbase == 0)
+                        return -1;
+                if(strncmp(old, new, obase-old) != 0)
+                        return -1;
+                nbase++;
+        } else {
+                if(nbase)
+                        return -1;
+                nbase = new;
+        }
+        nulldir(&d);
+        d.name = nbase;
+        return dirwstat(old, &d);
+}
+
+/*
+ *  see if a file exists
+ */
+extern int
+sysexist(char *file)
+{
+        Dir        *d;
+
+        d = dirstat(file);
+        if(d == nil)
+                return 0;
+        free(d);
+        return 1;
+}
+
+/*
+ *  return nonzero if file is a directory
+ */
+extern int
+sysisdir(char *file)
+{
+        Dir        *d;
+        int        rv;
+
+        d = dirstat(file);
+        if(d == nil)
+                return 0;
+        rv = d->mode & DMDIR;
+        free(d);
+        return rv;
+}
+
+/*
+ * kill a process or process group
+ */
+
+static int
+stomp(int pid, char *file)
+{
+        char name[64];
+        int fd;
+
+        snprint(name, sizeof(name), "/proc/%d/%s", pid, file);
+        fd = open(name, 1);
+        if(fd < 0)
+                return -1;
+        if(write(fd, "die: yankee pig dog\n", sizeof("die: yankee pig dog\n") - 1) <= 0){
+                close(fd);
+                return -1;
+        }
+        close(fd);
+        return 0;
+        
+}
+
+/*
+ *  kill a process
+ */
+extern int
+syskill(int pid)
+{
+        return stomp(pid, "note");
+        
+}
+
+/*
+ *  kill a process group
+ */
+extern int
+syskillpg(int pid)
+{
+        return stomp(pid, "notepg");
+}
+
+extern int
+sysdetach(void)
+{
+        if(rfork(RFENVG|RFNAMEG|RFNOTEG) < 0) {
+                werrstr("rfork failed");
+                return -1;
+        }
+        return 0;
+}
+
+/*
+ *  catch a write on a closed pipe
+ */
+static int *closedflag;
+static int
+catchpipe(void *a, char *msg)
+{
+        static char *foo = "sys: write on closed pipe";
+
+        USED(a);
+        if(strncmp(msg, foo, strlen(foo)) == 0){
+                if(closedflag)
+                        *closedflag = 1;
+                return 1;
+        }
+        return 0;
+}
+void
+pipesig(int *flagp)
+{
+        closedflag = flagp;
+        atnotify(catchpipe, 1);
+}
+void
+pipesigoff(void)
+{
+        atnotify(catchpipe, 0);
+}
+
+void
+exit9(int i)
+{
+        char buf[32];
+
+        if(i == 0)
+                exits(0);
+        snprint(buf, sizeof(buf), "%d", i);
+        exits(buf);
+}
+
+static int
+islikeatty(int fd)
+{
+        Dir *d;
+        int rv;
+
+        d = dirfstat(fd);
+        if(d == nil)
+                return 0;
+        rv = strcmp(d->name, "cons") == 0;
+        free(d);
+        return rv;
+}
+
+#if 0
+/* jpc */
+static int
+islikeatty(int fd)
+{
+        char buf[64];
+
+        if(fd2path(fd, buf, sizeof buf) != 0)
+                return 0;
+
+        /* might be /mnt/term/dev/cons */
+        return strlen(buf) >= 9 && strcmp(buf+strlen(buf)-9, "/dev/cons") == 0;
+}
+#endif
+
+extern int
+holdon(void)
+{
+        int fd;
+
+        if(!islikeatty(0))
+                return -1;
+
+        fd = open("/dev/consctl", OWRITE);
+        write(fd, "holdon", 6);
+
+        return fd;
+}
+
+extern int
+sysopentty(void)
+{
+        return open("/dev/cons", ORDWR);
+}
+
+extern void
+holdoff(int fd)
+{
+        write(fd, "holdoff", 7);
+        close(fd);
+}
+
+extern int
+sysfiles(void)
+{
+        return 128;
+}
+
+/*
+ *  expand a path relative to the user's mailbox directory
+ *
+ *  if the path starts with / or ./, don't change it
+ *
+ */
+extern String *
+mboxpath(char *path, char *user, String *to, int dot)
+{
+        if (dot || *path=='/' || strncmp(path, "./", 2) == 0
+                              || strncmp(path, "../", 3) == 0) {
+                to = s_append(to, path);
+        } else {
+                to = s_append(to, unsharp(MAILROOT));
+                to = s_append(to, "/box/");
+                to = s_append(to, user);
+                to = s_append(to, "/");
+                to = s_append(to, path);
+        }
+        return to;
+}
+
+extern String *
+mboxname(char *user, String *to)
+{
+        return mboxpath("mbox", user, to, 0);
+}
+
+extern String *
+deadletter(String *to)                /* pass in sender??? */
+{
+        char *cp;
+
+        cp = getlog();
+        if(cp == 0)
+                return 0;
+        return mboxpath("dead.letter", cp, to, 0);
+}
+
+char *
+homedir(char *user)
+{
+        USED(user);
+        return getenv("home");
+}
+
+String *
+readlock(String *file)
+{
+        char *cp;
+
+        cp = getlog();
+        if(cp == 0)
+                return 0;
+        return mboxpath("reading", cp, file, 0);
+}
+
+String *
+username(String *from)
+{
+        int n;
+        Biobuf *bp;
+        char *p, *q;
+        String *s;
+
+        bp = Bopen("/adm/keys.who", OREAD);
+        if(bp == 0)
+                bp = Bopen("/adm/netkeys.who", OREAD);
+        if(bp == 0)
+                return 0;
+
+        s = 0;
+        n = strlen(s_to_c(from));
+        for(;;) {
+                p = Brdline(bp, '\n');
+                if(p == 0)
+                        break;
+                p[Blinelen(bp)-1] = 0;
+                if(strncmp(p, s_to_c(from), n))
+                        continue;
+                p += n;
+                if(*p != ' ' && *p != '\t')        /* must be full match */
+                        continue;
+                while(*p && (*p == ' ' || *p == '\t'))
+                                p++;
+                if(*p == 0)
+                        continue;
+                for(q = p; *q; q++)
+                        if(('0' <= *q && *q <= '9') || *q == '<')
+                                break;
+                while(q > p && q[-1] != ' ' && q[-1] != '\t')
+                        q--;
+                while(q > p && (q[-1] == ' ' || q[-1] == '\t'))
+                        q--;
+                *q = 0;
+                s = s_new();
+                s_append(s, "\"");
+                s_append(s, p);
+                s_append(s, "\"");
+                break;
+        }
+        Bterm(bp);
+        return s;
+}
+
+char *
+remoteaddr(int fd, char *dir)
+{
+        char buf[128], *p;
+        int n;
+
+        if(dir == 0){
+                fprint(2,"remoteaddr: called fd2path: fixme\n"); /* jpc
+                if(fd2path(fd, buf, sizeof(buf)) != 0)
+                        return ""; */
+
+                /* parse something of the form /net/tcp/nnnn/data */
+                p = strrchr(buf, '/');
+                if(p == 0)
+                        return "";
+                strncpy(p+1, "remote", sizeof(buf)-(p-buf)-2);
+        } else
+                snprint(buf, sizeof buf, "%s/remote", dir);
+        buf[sizeof(buf)-1] = 0;
+
+        fd = open(buf, OREAD);
+        if(fd < 0)
+                return "";
+        n = read(fd, buf, sizeof(buf)-1);
+        close(fd);
+        if(n > 0){
+                buf[n] = 0;
+                p = strchr(buf, '!');
+                if(p)
+                        *p = 0;
+                return strdup(buf);
+        }
+        return "";
+}
+
+//  create a file and 
+//        1) ensure the modes we asked for
+//        2) make gid == uid
+static int
+docreate(char *file, int perm)
+{
+        int fd;
+        Dir ndir;
+        Dir *d;
+
+        //  create the mbox
+        fd = create(file, OREAD, perm);
+        if(fd < 0){
+                fprint(2, "couldn't create %s\n", file);
+                return -1;
+        }
+        d = dirfstat(fd);
+        if(d == nil){
+                fprint(2, "couldn't stat %s\n", file);
+                return -1;
+        }
+        nulldir(&ndir);
+        ndir.mode = perm;
+        ndir.gid = d->uid;
+        if(dirfwstat(fd, &ndir) < 0)
+                fprint(2, "couldn't chmod %s: %r\n", file);
+        close(fd);
+        return 0;
+}
+
+//  create a mailbox
+int
+creatembox(char *user, char *folder)
+{
+        char *p;
+        String *mailfile;
+        char buf[512];
+        Mlock *ml;
+
+        mailfile = s_new();
+        if(folder == 0)
+                mboxname(user, mailfile);
+        else {
+                snprint(buf, sizeof(buf), "%s/mbox", folder);
+                mboxpath(buf, user, mailfile, 0);
+        }
+
+        // don't destroy existing mailbox
+        if(access(s_to_c(mailfile), 0) == 0){
+                fprint(2, "mailbox already exists\n");
+                return -1;
+        }
+        fprint(2, "creating new mbox: %s\n", s_to_c(mailfile));
+
+        //  make sure preceding levels exist
+        for(p = s_to_c(mailfile); p; p++) {
+                if(*p == '/')        /* skip leading or consecutive slashes */
+                        continue;
+                p = strchr(p, '/');
+                if(p == 0)
+                        break;
+                *p = 0;
+                if(access(s_to_c(mailfile), 0) != 0){
+                        if(docreate(s_to_c(mailfile), DMDIR|0711) < 0)
+                                return -1;
+                }
+                *p = '/';
+        }
+
+        //  create the mbox
+        if(docreate(s_to_c(mailfile), 0622|DMAPPEND|DMEXCL) < 0)
+                return -1;
+
+        /*
+         *  create the lock file if it doesn't exist
+         */
+        ml = trylock(s_to_c(mailfile));
+        if(ml != nil)
+                sysunlock(ml);
+
+        return 0;
+}
diff --git a/src/cmd/upas/common/mail.c b/src/cmd/upas/common/mail.c
t@@ -0,0 +1,57 @@
+#include "common.h"
+
+/* format of REMOTE FROM lines */
+char *REMFROMRE =
+        "^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)[ \t]+remote[ \t]+from[ \t]+(.*)\n$";
+int REMSENDERMATCH = 1;
+int REMDATEMATCH = 4;
+int REMSYSMATCH = 5;
+
+/* format of LOCAL FROM lines */
+char *FROMRE =
+        "^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)\n$";
+int SENDERMATCH = 1;
+int DATEMATCH = 4;
+
+/* output a unix style local header */
+int
+print_header(Biobuf *fp, char *sender, char *date)
+{
+        return Bprint(fp, "From %s %s\n", sender, date);
+}
+
+/* output a unix style remote header */
+int
+print_remote_header(Biobuf *fp, char *sender, char *date, char *system)
+{
+        return Bprint(fp, "From %s %s remote from %s\n", sender, date, system);
+}
+
+/* parse a mailbox style header */
+int
+parse_header(char *line, String *sender, String *date)
+{
+        if (!IS_HEADER(line))
+                return -1;
+        line += sizeof("From ") - 1;
+        s_restart(sender);
+        while(*line==' '||*line=='\t')
+                line++;
+        if(*line == '"'){
+                s_putc(sender, *line++);
+                while(*line && *line != '"')
+                        s_putc(sender, *line++);
+                s_putc(sender, *line++);
+        } else {
+                while(*line && *line != ' ' && *line != '\t')
+                        s_putc(sender, *line++);
+        }
+        s_terminate(sender);
+        s_restart(date);
+        while(*line==' '||*line=='\t')
+                line++;
+        while(*line)
+                s_putc(date, *line++);
+        s_terminate(date);
+        return 0;
+}
diff --git a/src/cmd/upas/common/makefile b/src/cmd/upas/common/makefile
t@@ -0,0 +1,18 @@
+CFLAGS=${UNIX} -g -I. -I../libc -I../common -I/usr/include ${SCFLAGS}
+OBJS=mail.o aux.o string.o ${SYSOBJ}
+AR=ar
+.c.o: ; ${CC} -c ${CFLAGS} $*.c
+
+common.a: ${OBJS}
+        ${AR} cr common.a ${OBJS}
+        -ranlib common.a
+
+aux.o:                aux.h string.h mail.h
+string.o:        string.h mail.h
+mail.o:                mail.h
+syslog.o:        sys.h
+mail.h:                sys.h
+
+clean:
+        -rm -f *.[oO] core a.out *.a *.sL common.a
+
diff --git a/src/cmd/upas/common/mkfile b/src/cmd/upas/common/mkfile
t@@ -0,0 +1,20 @@
+<$PLAN9/src/mkhdr
+
+LIB=libcommon.a
+
+OFILES=aux.$O\
+        become.$O\
+        mail.$O\
+        process.$O\
+        libsys.$O\
+        config.$O\
+        appendfiletombox.$O\
+
+HFILES=common.h\
+        sys.h\
+
+<$PLAN9/src/mklib
+
+nuke:V:
+        mk clean
+        rm -f libcommon.a
diff --git a/src/cmd/upas/common/process.c b/src/cmd/upas/common/process.c
t@@ -0,0 +1,175 @@
+#include "common.h"
+
+/* make a stream to a child process */
+extern stream *
+instream(void)
+{
+        stream *rv;
+        int pfd[2];
+
+        if ((rv = (stream *)malloc(sizeof(stream))) == 0)
+                return 0;
+        memset(rv, 0, sizeof(stream));
+        if (pipe(pfd) < 0)
+                return 0;
+        if(Binit(&rv->bb, pfd[1], OWRITE) < 0){
+                close(pfd[0]);
+                close(pfd[1]);
+                return 0;
+        }
+        rv->fp = &rv->bb;
+        rv->fd = pfd[0];        
+        return rv;
+}
+
+/* make a stream from a child process */
+extern stream *
+outstream(void)
+{
+        stream *rv;
+        int pfd[2];
+
+        if ((rv = (stream *)malloc(sizeof(stream))) == 0)
+                return 0;
+        memset(rv, 0, sizeof(stream));
+        if (pipe(pfd) < 0)
+                return 0;
+        if (Binit(&rv->bb, pfd[0], OREAD) < 0){
+                close(pfd[0]);
+                close(pfd[1]);
+                return 0;
+        }
+        rv->fp = &rv->bb;
+        rv->fd = pfd[1];
+        return rv;
+}
+
+extern void
+stream_free(stream *sp)
+{
+        int fd;
+
+        close(sp->fd);
+        fd = Bfildes(sp->fp);
+        Bterm(sp->fp);
+        close(fd);
+        free((char *)sp);
+}
+
+/* start a new process */
+extern process *
+noshell_proc_start(char **av, stream *inp, stream *outp, stream *errp, int newpg, char *who)
+{
+        process *pp;
+        int i, n;
+
+        if ((pp = (process *)malloc(sizeof(process))) == 0) {
+                if (inp != 0)
+                        stream_free(inp);
+                if (outp != 0)
+                        stream_free(outp);
+                if (errp != 0)
+                        stream_free(errp);
+                return 0;
+        }
+        pp->std[0] = inp;
+        pp->std[1] = outp;
+        pp->std[2] = errp;
+        switch (pp->pid = fork()) {
+        case -1:
+                proc_free(pp);
+                return 0;
+        case 0:
+                if(newpg)
+                        sysdetach();
+                for (i=0; i<3; i++)
+                        if (pp->std[i] != 0){
+                                close(Bfildes(pp->std[i]->fp));
+                                while(pp->std[i]->fd < 3)
+                                        pp->std[i]->fd = dup(pp->std[i]->fd, -1);
+                        }
+                for (i=0; i<3; i++)
+                        if (pp->std[i] != 0)
+                                dup(pp->std[i]->fd, i);
+                for (n = sysfiles(); i < n; i++)
+                        close(i);
+                if(who) {
+                        fprint(2,"process.c: trying to become(%s,%s)\n",av,who);
+                        // jpc become(av, who);
+                }
+                exec(av[0], av);
+                perror("proc_start");
+                exits("proc_start");
+        default:
+                for (i=0; i<3; i++)
+                        if (pp->std[i] != 0) {
+                                close(pp->std[i]->fd);
+                                pp->std[i]->fd = -1;
+                        }
+                return pp;
+        }
+}
+
+/* start a new process under a shell */
+extern process *
+proc_start(char *cmd, stream *inp, stream *outp, stream *errp, int newpg, char *who)
+{
+        char *av[4];
+
+        av[0] = unsharp(SHELL);
+        av[1] = "-c";
+        av[2] = cmd;
+        av[3] = 0;
+        return noshell_proc_start(av, inp, outp, errp, newpg, who);
+}
+
+/* wait for a process to stop */
+extern int
+proc_wait(process *pp)
+{
+        Waitmsg *status;
+        char err[Errlen];
+
+        for(;;){
+                status = wait();
+                if(status == nil){
+                        errstr(err, sizeof(err));
+                        if(strstr(err, "interrupt") == 0)
+                                break;
+                }
+                if (status->pid==pp->pid)
+                        break;
+        }
+        pp->pid = -1;
+        if(status == nil)
+                pp->status = -1;
+        else
+                pp->status = status->msg[0];
+        pp->waitmsg = status;
+        return pp->status;
+}
+
+/* free a process */
+extern int
+proc_free(process *pp)
+{
+        int i;
+
+        if(pp->std[1] == pp->std[2])
+                pp->std[2] = 0;                /* avoid freeing it twice */
+        for (i = 0; i < 3; i++)
+                if (pp->std[i])
+                        stream_free(pp->std[i]);
+        if (pp->pid >= 0)
+                proc_wait(pp);
+        free(pp->waitmsg);
+        free((char *)pp);
+        return 0;
+}
+
+/* kill a process */
+extern int
+proc_kill(process *pp)
+{
+        return syskill(pp->pid);
+}
diff --git a/src/cmd/upas/common/sys.h b/src/cmd/upas/common/sys.h
t@@ -0,0 +1,85 @@
+/*
+ * System dependent header files for research
+ */
+
+#include 
+#include 
+#include 
+#include 
+#include "libString.h"   /* jpc String.h -> libString.h */
+
+/*
+ *  for the lock routines in libsys.c
+ */
+typedef struct Mlock        Mlock;
+struct Mlock {
+        int fd;
+        int pid;
+        String *name;
+};
+
+/*
+ *  from config.c
+ */
+extern char *MAILROOT;        /* root of mail system */
+extern char *UPASLOG;        /* log directory */
+extern char *UPASLIB;        /* upas library directory */
+extern char *UPASBIN;        /* upas binary directory */
+extern char *UPASTMP;        /* temporary directory */
+extern char *SHELL;        /* path name of shell */
+extern char *POST;        /* path name of post server addresses */
+extern int MBOXMODE;        /* default mailbox protection mode */
+
+/*
+ *  files in libsys.c
+ */
+extern char        *sysname_read(void);
+extern char        *alt_sysname_read(void);
+extern char        *domainname_read(void);
+extern char        **sysnames_read(void);
+extern char        *getlog(void);
+extern char        *thedate(void);
+extern Biobuf        *sysopen(char*, char*, ulong);
+extern int        sysopentty(void);
+extern int        sysclose(Biobuf*);
+extern int        sysmkdir(char*, ulong);
+extern int        syschgrp(char*, char*);
+extern Mlock        *syslock(char *);
+extern void        sysunlock(Mlock *);
+extern void        syslockrefresh(Mlock *);
+extern int        e_nonexistent(void);
+extern int        e_locked(void);
+extern long        sysfilelen(Biobuf*);
+extern int        sysremove(char*);
+extern int        sysrename(char*, char*);
+extern int        sysexist(char*);
+extern int        sysisdir(char*);
+extern int        syskill(int);
+extern int        syskillpg(int);
+extern int        syscreate(char*, int, ulong);
+extern Mlock        *trylock(char *);
+extern void        exit9(int);
+extern void        pipesig(int*);
+extern void        pipesigoff(void);
+extern int        holdon(void);
+extern void        holdoff(int);
+extern int        syscreatelocked(char*, int, int);
+extern int        sysopenlocked(char*, int);
+extern int        sysunlockfile(int);
+extern int        sysfiles(void);
+extern int         become(char**, char*);
+extern int        sysdetach(void);
+extern int        sysdirreadall(int, Dir**);
+extern String        *username(String*);
+extern char*        remoteaddr(int, char*);
+extern int        creatembox(char*, char*);
+
+extern String        *readlock(String*);
+extern char        *homedir(char*);
+extern String        *mboxname(char*, String*);
+extern String        *deadletter(String*);
+
+/*
+ *  maximum size for a file path
+ */
+#define MAXPATHLEN 128
diff --git a/src/cmd/upas/filterkit/dat.h b/src/cmd/upas/filterkit/dat.h
t@@ -0,0 +1,8 @@
+typedef struct Addr Addr;
+struct Addr
+{
+        Addr *next;
+        char *val;
+};
+
+extern Addr* readaddrs(char*, Addr*);
diff --git a/src/cmd/upas/filterkit/deliver.c b/src/cmd/upas/filterkit/deliver.c
t@@ -0,0 +1,60 @@
+#include "dat.h"
+#include "common.h"
+
+void
+usage(void)
+{
+        fprint(2, "usage: %s recipient fromaddr-file mbox\n", argv0);
+        exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+        int fd;
+        char now[30];
+        Addr *a;
+        char *deliveredto;
+        Mlock *l;
+        int bytes;
+
+        ARGBEGIN{
+        }ARGEND;
+
+        if(argc != 3)
+                usage();
+
+        deliveredto = strrchr(argv[0], '!');
+        if(deliveredto == nil)
+                deliveredto = argv[0];
+        else
+                deliveredto++;
+        a = readaddrs(argv[1], nil);
+        if(a == nil)
+                sysfatal("missing from address");
+
+        l = syslock(argv[2]);
+
+        /* append to mbox */
+        fd = open(argv[2], OWRITE);
+        if(fd < 0)
+                sysfatal("opening mailbox: %r");
+        seek(fd, 0, 2);
+        strncpy(now, ctime(time(0)), sizeof(now));
+        now[28] = 0;
+        if(fprint(fd, "From %s %s\n", a->val, now) < 0)
+                sysfatal("writing mailbox: %r");
+
+        /* copy message handles escapes and any needed new lines */
+        bytes = appendfiletombox(0, fd);
+        if(bytes < 0)
+                sysfatal("writing mailbox: %r");
+
+        close(fd);
+        sysunlock(l);
+
+        /* log it */
+        syslog(0, "mail", "delivered %s From %s %s (%s) %d", deliveredto,
+                a->val, now, argv[0], bytes);
+        exits(0);
+}
diff --git a/src/cmd/upas/filterkit/list.c b/src/cmd/upas/filterkit/list.c
t@@ -0,0 +1,315 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "dat.h"
+
+int debug;
+
+enum
+{
+        Tregexp=        (1<<0),                /* ~ */
+        Texact=                (1<<1),                /* = */
+};
+
+typedef struct Pattern Pattern;
+struct Pattern
+{
+        Pattern        *next;
+        int        type;
+        char        *arg;
+        int        bang;
+};
+
+String        *patternpath;
+Pattern        *patterns;
+String        *mbox;
+
+static void
+usage(void)
+{
+        fprint(2, "usage: %s 'check|add' patternfile addr [addr*]\n", argv0);
+        exits("usage");
+}
+
+/*
+ *  convert string to lower case
+ */
+static void
+mklower(char *p)
+{
+        int c;
+
+        for(; *p; p++){
+                c = *p;
+                if(c <= 'Z' && c >= 'A')
+                        *p = c - 'A' + 'a';
+        }
+}
+
+/*
+ *  simplify an address, reduce to a domain
+ */
+static String*
+simplify(char *addr)
+{
+        int dots;
+        char *p, *at;
+        String *s;
+
+        mklower(addr);
+        at = strchr(addr, '@');
+        if(at == nil){
+                /* local address, make it an exact match */
+                s = s_copy("=");
+                s_append(s, addr);
+                return s;
+        }
+
+        /* copy up to the '@' sign */
+        at++;
+        s = s_copy("~");
+        for(p = addr; p < at; p++){
+                if(strchr(".*+?(|)\\[]^$", *p))
+                        s_putc(s, '\\');
+                s_putc(s, *p);
+        }
+
+        /* just any address matching the two most significant domain elements */
+        s_append(s, "(.*\\.)?");
+        p = addr+strlen(addr);
+        dots = 0;
+        for(; p > at; p--){
+                if(*p != '.')
+                        continue;
+                if(dots++ > 0){
+                        p++;
+                        break;
+                }
+        }
+        for(; *p; p++){
+                if(strchr(".*+?(|)\\[]^$", *p) != 0)
+                        s_putc(s, '\\');
+                s_putc(s, *p);
+        }
+        s_terminate(s);
+
+        return s;
+}
+
+/*
+ *  link patterns in order
+ */
+static int
+newpattern(int type, char *arg, int bang)
+{
+        Pattern *p;
+        static Pattern *last;
+
+        mklower(arg);
+
+        p = mallocz(sizeof *p, 1);
+        if(p == nil)
+                return -1;
+        if(type == Tregexp){
+                p->arg = malloc(strlen(arg)+3);
+                if(p->arg == nil){
+                        free(p);
+                        return -1;
+                }
+                p->arg[0] = 0;
+                strcat(p->arg, "^");
+                strcat(p->arg, arg);
+                strcat(p->arg, "$");
+        } else {
+                p->arg = strdup(arg);
+                if(p->arg == nil){
+                        free(p);
+                        return -1;
+                }
+        }
+        p->type = type;
+        p->bang = bang;
+        if(last == nil)
+                patterns = p;
+        else
+                last->next = p;
+        last = p;
+
+        return 0;
+}
+
+/*
+ *  patterns are either
+ *        ~ regular expression
+ *        = exact match string
+ *
+ *  all comparisons are case insensitive
+ */
+static int
+readpatterns(char *path)
+{
+        Biobuf *b;
+        char *p;
+        char *token[2];
+        int n;
+        int bang;
+
+        b = Bopen(path, OREAD);
+        if(b == nil)
+                return -1;
+        while((p = Brdline(b, '\n')) != nil){
+                p[Blinelen(b)-1] = 0;
+                n = tokenize(p, token, 2);
+                if(n == 0)
+                        continue;
+
+                mklower(token[0]);
+                p = token[0];
+                if(*p == '!'){
+                        p++;
+                        bang = 1;
+                } else
+                        bang = 0;
+
+                if(*p == '='){
+                        if(newpattern(Texact, p+1, bang) < 0)
+                                return -1;
+                } else if(*p == '~'){
+                        if(newpattern(Tregexp, p+1, bang) < 0)
+                                return -1;
+                } else if(strcmp(token[0], "#include") == 0 && n == 2)
+                        readpatterns(token[1]);
+        }
+        Bterm(b);
+        return 0;
+}
+
+/* fuck, shit, bugger, damn */
+void regerror(char*)
+{
+}
+
+/*
+ *  check lower case version of address agains patterns
+ */
+static Pattern*
+checkaddr(char *arg)
+{
+        Pattern *p;
+        Reprog *rp;
+        String *s;
+
+        s = s_copy(arg);
+        mklower(s_to_c(s));
+
+        for(p = patterns; p != nil; p = p->next)
+                switch(p->type){
+                case Texact:
+                        if(strcmp(p->arg, s_to_c(s)) == 0){
+                                free(s);
+                                return p;
+                        }
+                        break;
+                case Tregexp:
+                        rp = regcomp(p->arg);
+                        if(rp == nil)
+                                continue;
+                        if(regexec(rp, s_to_c(s), nil, 0)){
+                                free(rp);
+                                free(s);
+                                return p;
+                        }
+                        free(rp);
+                        break;
+                }
+        s_free(s);
+        return 0;
+}
+static char*
+check(int argc, char **argv)
+{
+        int i;
+        Addr *a;
+        Pattern *p;
+        int matchedbang;
+
+        matchedbang = 0;
+        for(i = 0; i < argc; i++){
+                a = readaddrs(argv[i], nil);
+                for(; a != nil; a = a->next){
+                        p = checkaddr(a->val);
+                        if(p == nil)
+                                continue;
+                        if(p->bang)
+                                matchedbang = 1;
+                        else
+                                return nil;
+                }
+        }
+        if(matchedbang)
+                return "!match";
+        else
+                return "no match";
+}
+
+/*
+ *  add anything that isn't already matched, all matches are lower case
+ */
+static char*
+add(char *pp, int argc, char **argv)
+{
+        int fd, i;
+        String *s;
+        char *cp;
+        Addr *a;
+
+        a = nil;
+        for(i = 0; i < argc; i++)
+                a = readaddrs(argv[i], a);
+
+        fd = open(pp, OWRITE);
+        seek(fd, 0, 2);
+        for(; a != nil; a = a->next){
+                if(checkaddr(a->val))
+                        continue;
+                s = simplify(a->val);
+                cp = s_to_c(s);
+                fprint(fd, "%q\t%q\n", cp, a->val);
+                if(*cp == '=')
+                        newpattern(Texact, cp+1, 0);
+                else if(*cp == '~')
+                        newpattern(Tregexp, cp+1, 0);
+                s_free(s);
+        }
+        close(fd);
+        return nil;        
+}
+
+void
+main(int argc, char **argv)
+{
+        char *patternpath;
+
+        ARGBEGIN {
+        case 'd':
+                debug++;
+                break;
+        } ARGEND;
+
+        quotefmtinstall();
+
+        if(argc < 3)
+                usage();
+
+        patternpath = argv[1];
+        readpatterns(patternpath);
+        if(strcmp(argv[0], "add") == 0)
+                exits(add(patternpath, argc-2, argv+2));
+        else if(strcmp(argv[0], "check") == 0)
+                exits(check(argc-2, argv+2));
+        else
+                usage();
+}
diff --git a/src/cmd/upas/filterkit/mkfile b/src/cmd/upas/filterkit/mkfile
t@@ -0,0 +1,21 @@
+
diff --git a/src/cmd/upas/filterkit/pipefrom.sample b/src/cmd/upas/filterkit/pipefrom.sample
t@@ -0,0 +1,24 @@
+#!/bin/rc
+
+rfork e
+TMP=/tmp/myupassend.$pid
+
+# collect upas/send options
+options=()
+while (! ~ $#* 0 && ~ $1 -*) {
+        options=($options $1);
+        shift
+}
+
+# collect addresses and add them to my patterns
+dests=()
+while (! ~ $#* 0) {
+        dests=($dests $1);
+        shift
+}
+echo $dests > $TMP
+upas/list add /mail/box/$user/_pattern $TMP >[2] /dev/null
+rm $TMP
+
+# send mail
+upas/send $options $dests
diff --git a/src/cmd/upas/filterkit/pipeto.sample b/src/cmd/upas/filterkit/pipeto.sample
t@@ -0,0 +1,73 @@
+#!/bin/rc
+
+# create a /tmp for here documents
+rfork en
+bind -c /mail/tmp /tmp
+
+KEY=whocares
+USER=ken
+
+RECIP=$1
+MBOX=$2
+PF=/mail/box/$USER/_pattern
+TMP=/mail/tmp/mine.$pid
+BIN=/bin/upas
+D=/mail/fs/mbox/1
+
+# save and parse the mail file
+{sed '/^$/,$ s/^From / From /'; echo} > $TMP
+upas/fs -f $TMP
+
+# if we like the source
+# or if the subject contains a valid token
+# then deliver the mail and allow all the addresses
+if( $BIN/list check $PF $D/from $D/sender $D/replyto )
+{
+        $BIN/deliver $RECIP $D/from $MBOX < $D/raw
+        $BIN/list add $PF $D/from $D/to $D/cc $D/sender
+        rm $TMP
+        exit 0
+}
+switch($status){
+case *!match*
+        echo `{date} dropped $RECIP From `{cat $D/replyto} >> /mail/box/$USER/_bounced >[2] /dev/null
+        rm $TMP
+        exit 0
+}
+if ( $BIN/token $KEY $D/subject )
+{
+        $BIN/deliver $RECIP $D/from $MBOX < $D/raw
+        $BIN/list add $PF $D/from $D/to $D/cc $D/sender
+        rm $TMP
+        echo `{date} added $RECIP From `{cat $D/replyto} \
+                >> /mail/box/$USER/_bounced >[2] /dev/null
+        exit 0
+}
+
+# don't recognize the sender so
+# return the message with instructions
+TOKEN=`{upas/token $KEY}
+upasname=/dev/null
+{{cat; cat $D/raw} | upas/send `{cat $D/replyto}}<> /mail/box/$USER/_bounced >[2] /dev/null
+
+rv=$status
+rm $TMP
+exit $status
diff --git a/src/cmd/upas/filterkit/pipeto.sample-hold b/src/cmd/upas/filterkit/pipeto.sample-hold
t@@ -0,0 +1,43 @@
+#!/bin/rc
+
+# create a /tmp for here documents
+rfork en
+bind -c /mail/tmp /tmp
+
+KEY=whocares
+USER=ken
+
+RECIP=$1
+MBOX=$2
+PF=/mail/box/$USER/_pattern
+TMP=/mail/tmp/mine.$pid
+BIN=/bin/upas
+D=/mail/fs/mbox/1
+
+# save and parse the mail file
+{sed '/^$/,$ s/^From / From /'; echo} > $TMP
+upas/fs -f $TMP
+
+# if we like the source
+# or if the subject contains a valid token
+# then deliver the mail and allow all the addresses
+if( $BIN/list check $PF $D/from $D/sender $D/replyto )
+{
+        $BIN/deliver $RECIP $D/from $MBOX < $D/raw
+        $BIN/list add $PF $D/from $D/to $D/cc $D/sender
+        rm $TMP
+        exit 0
+}
+switch($status){
+case *!match*
+        echo `{date} dropped $RECIP From `{cat $D/replyto} >> /mail/box/$USER/_bounced >[2] /dev/null
+        rm $TMP
+        exit 0
+}
+
+# don't recognize the sender so hold the message
+$BIN/deliver $RECIP $D/from /mail/box/$USER/_held < $D/raw
+
+rv=$status
+rm $TMP
+exit $status
diff --git a/src/cmd/upas/filterkit/readaddrs.c b/src/cmd/upas/filterkit/readaddrs.c
t@@ -0,0 +1,98 @@
+#include 
+#include 
+#include "dat.h"
+
+void*
+emalloc(int size)
+{
+        void *a;
+
+        a = mallocz(size, 1);
+        if(a == nil)
+                sysfatal("%r");
+        return a;
+}
+
+char*
+estrdup(char *s)
+{
+        s = strdup(s);
+        if(s == nil)
+                sysfatal("%r");
+        return s;
+}
+
+/*
+ * like tokenize but obey "" quoting
+ */
+int
+tokenize822(char *str, char **args, int max)
+{
+        int na;
+        int intok = 0, inquote = 0;
+
+        if(max <= 0)
+                return 0;        
+        for(na=0; ;str++)
+                switch(*str) {
+                case ' ':
+                case '\t':
+                        if(inquote)
+                                goto Default;
+                        /* fall through */
+                case '\n':
+                        *str = 0;
+                        if(!intok)
+                                continue;
+                        intok = 0;
+                        if(na < max)
+                                continue;
+                        /* fall through */
+                case 0:
+                        return na;
+                case '"':
+                        inquote ^= 1;
+                        /* fall through */
+                Default:
+                default:
+                        if(intok)
+                                continue;
+                        args[na++] = str;
+                        intok = 1;
+                }
+        return 0;        /* can't get here; silence compiler */
+}
+
+Addr*
+readaddrs(char *file, Addr *a)
+{
+        int fd;
+        int i, n;
+        char buf[8*1024];
+        char *f[128];
+        Addr **l;
+        Addr *first;
+
+        /* add to end */
+        first = a;
+        for(l = &first; *l != nil; l = &(*l)->next)
+                ;
+
+        /* read in the addresses */
+        fd = open(file, OREAD);
+        if(fd < 0)
+                return first;
+        n = read(fd, buf, sizeof(buf)-1);
+        close(fd);
+        if(n <= 0)
+                return first;
+        buf[n] = 0;
+
+        n = tokenize822(buf, f, nelem(f));
+        for(i = 0; i < n; i++){
+                *l = a = emalloc(sizeof *a);
+                l = &a->next;
+                a->val = estrdup(f[i]);
+        }
+        return first;
+}
diff --git a/src/cmd/upas/filterkit/token.c b/src/cmd/upas/filterkit/token.c
t@@ -0,0 +1,89 @@
+#include 
+#include 
+#include 
+#include 
+#include "dat.h"
+
+void
+usage(void)
+{
+        fprint(2, "usage: %s key [token]\n", argv0);
+        exits("usage");
+}
+
+static String*
+mktoken(char *key, long thetime)
+{
+        char *now;
+        uchar digest[SHA1dlen];
+        char token[64];
+        String *s;
+        
+        now = ctime(thetime);
+        memset(now+11, ':', 8);
+        hmac_sha1((uchar*)now, strlen(now), (uchar*)key, strlen(key), digest, nil);
+        enc64(token, sizeof token, digest, sizeof digest);
+        s = s_new();
+        s_nappend(s, token, 5);
+        return s;
+}
+
+static char*
+check_token(char *key, char *file)
+{
+        String *s;
+        long now;
+        int i;
+        char buf[1024];
+        int fd;
+
+        fd = open(file, OREAD);
+        if(fd < 0)
+                return "no match";
+        i = read(fd, buf, sizeof(buf)-1);
+        close(fd);
+        if(i < 0)
+                return "no match";
+        buf[i] = 0;
+        
+        now = time(0);
+
+        for(i = 0; i < 14; i++){
+                s = mktoken(key, now-24*60*60*i);
+                if(strstr(buf, s_to_c(s)) != nil){
+                        s_free(s);
+                        return nil;
+                }
+                s_free(s);
+        }
+        return "no match";
+}
+
+static char*
+create_token(char *key)
+{
+        String *s;
+
+        s = mktoken(key, time(0));
+        print("%s", s_to_c(s));
+        return nil;
+}
+
+void
+main(int argc, char **argv)
+{
+        ARGBEGIN {
+        } ARGEND;
+
+        switch(argc){
+        case 2:
+                exits(check_token(argv[0], argv[1]));
+                break;
+        case 1:
+                exits(create_token(argv[0]));
+                break;
+        default:
+                usage();
+        }
+        exits(0);
+}
diff --git a/src/cmd/upas/fs/dat.h b/src/cmd/upas/fs/dat.h
t@@ -0,0 +1,221 @@
+typedef struct Message Message;
+struct Message
+{
+        int        id;
+        int        refs;
+        int        subname;
+        char        name[Elemlen];
+
+        // pointers into message
+        char        *start;                // start of message
+        char        *end;                // end of message
+        char        *header;        // start of header
+        char        *hend;                // end of header
+        int        hlen;                // length of header minus ignored fields
+        char        *mheader;        // start of mime header
+        char        *mhend;                // end of mime header
+        char        *body;                // start of body
+        char        *bend;                // end of body
+        char        *rbody;                // raw (unprocessed) body
+        char        *rbend;                // end of raw (unprocessed) body
+        char        *lim;
+        char        deleted;
+        char        inmbox;
+        char        mallocd;        // message is malloc'd
+        char        ballocd;        // body is malloc'd
+        char        hallocd;        // header is malloce'd
+
+        // mail info
+        String        *unixheader;
+        String        *unixfrom;
+        String        *unixdate;
+        String        *from822;
+        String        *sender822;
+        String        *to822;
+        String        *bcc822;
+        String        *cc822;
+        String        *replyto822;
+        String        *date822;
+        String        *inreplyto822;
+        String        *subject822;
+        String        *messageid822;
+        String        *addrs;
+        String        *mimeversion;
+        String        *sdigest;
+
+        // mime info
+        String        *boundary;
+        String        *type;
+        int        encoding;
+        int        disposition;
+        String        *charset;
+        String        *filename;
+        int        converted;
+        int        decoded;
+        char        lines[10];        // number of lines in rawbody
+
+        Message        *next;                // same level
+        Message        *part;                // down a level
+        Message        *whole;                // up a level
+
+        uchar        digest[SHA1dlen];
+
+        vlong        imapuid;        // used by imap4
+
+        char                uidl[80];        // used by pop3
+        int                mesgno;
+};
+
+enum
+{
+        // encodings
+        Enone=        0,
+        Ebase64,
+        Equoted,
+
+        // disposition possibilities
+        Dnone=        0,
+        Dinline,
+        Dfile,
+        Dignore,
+
+        PAD64=        '=',
+};
+
+typedef struct Mailbox Mailbox;
+struct Mailbox
+{
+        QLock ql;      /* jpc named Qlock */
+        int        refs;
+        Mailbox        *next;
+        int        id;
+        int        dolock;                // lock when syncing?
+        int        std;
+        char        name[Elemlen];
+        char        path[Pathlen];
+        Dir        *d;
+        Message        *root;
+        int        vers;                // goes up each time mailbox is read
+
+        ulong waketime;
+        char        *(*sync)(Mailbox*, int);
+        void        (*close)(Mailbox*);
+        char        *(*fetch)(Mailbox*, Message*);
+        char        *(*ctl)(Mailbox*, int, char**);
+        void        *aux;                // private to Mailbox implementation
+};
+
+typedef char *Mailboxinit(Mailbox*, char*);
+
+extern Message        *root;
+extern Mailboxinit        plan9mbox;
+extern Mailboxinit        pop3mbox;
+extern Mailboxinit        imap4mbox;
+
+char*                syncmbox(Mailbox*, int);
+char*                geterrstr(void);
+void*                emalloc(ulong);
+void*                erealloc(void*, ulong);
+Message*        newmessage(Message*);
+void                delmessage(Mailbox*, Message*);
+void                delmessages(int, char**);
+int                newid(void);
+void                mailplumb(Mailbox*, Message*, int);
+char*                newmbox(char*, char*, int);
+void                freembox(char*);
+void                logmsg(char*, Message*);
+void                msgincref(Message*);
+void                msgdecref(Mailbox*, Message*);
+void                mboxincref(Mailbox*);
+void                mboxdecref(Mailbox*);
+void                convert(Message*);
+void                decode(Message*);
+int                cistrncmp(char*, char*, int);
+int                cistrcmp(char*, char*);
+int                latin1toutf(char*, char*, char*);
+int                windows1257toutf(char*, char*, char*);
+int                decquoted(char*, char*, char*);
+int                xtoutf(char*, char**, char*, char*);
+void                countlines(Message*);
+int                headerlen(Message*);
+void                parse(Message*, int, Mailbox*, int);
+void                parseheaders(Message*, int, Mailbox*, int);
+void                parsebody(Message*, Mailbox*);
+void                parseunix(Message*);
+String*        date822tounix(char*);
+int                fidmboxrefs(Mailbox*);
+int                hashmboxrefs(Mailbox*);
+void                checkmboxrefs(void);
+
+extern int        debug;
+extern int        fflag;
+extern int        logging;
+extern char        user[Elemlen];
+extern char        stdmbox[Pathlen];
+extern QLock        mbllock;
+extern Mailbox        *mbl;
+extern char        *mntpt;
+extern int        biffing;
+extern int        plumbing;
+extern char*        Enotme;
+
+enum
+{
+        /* mail subobjects */
+        Qbody,
+        Qbcc,
+        Qcc,
+        Qdate,
+        Qdigest,
+        Qdisposition,
+        Qfilename,
+        Qfrom,
+        Qheader,
+        Qinreplyto,
+        Qlines,
+        Qmimeheader,
+        Qmessageid,
+        Qraw,
+        Qrawbody,
+        Qrawheader,
+        Qrawunix,
+        Qreplyto,
+        Qsender,
+        Qsubject,
+        Qto,
+        Qtype,
+        Qunixheader,
+        Qinfo,
+        Qunixdate,
+        Qmax,
+
+        /* other files */
+        Qtop,
+        Qmbox,
+        Qdir,
+        Qctl,
+        Qmboxctl,
+};
+
+#define PATH(id, f)        ((((id)&0xfffff)<<10) | (f))
+#define FILE(p)                ((p) & 0x3ff)
+
+/* char *dirtab[]; jpc */
+
+// hash table to aid in name lookup, all files have an entry
+typedef struct Hash Hash;
+struct Hash {
+        Hash        *next;
+        char        *name;
+        ulong        ppath;
+        Qid        qid;
+        Mailbox        *mb;
+        Message        *m;
+};
+
+Hash        *hlook(ulong, char*);
+void        henter(ulong, char*, Qid, Message*, Mailbox*);
+void        hfree(ulong, char*);
+
+ulong msgallocd, msgfreed;
+
diff --git a/src/cmd/upas/fs/fs.c b/src/cmd/upas/fs/fs.c
t@@ -0,0 +1,1704 @@
+#include "common.h"
+#include 
+#include 
+#include 
+#include <9pclient.h> /* jpc */
+#include  /* jpc */
+#include "dat.h"
+
+enum
+{
+        OPERM        = 0x3,                // mask of all permission types in open mode
+};
+
+typedef struct Fid Fid;
+
+struct Fid
+{
+        Qid        qid;
+        short        busy;
+        short        open;
+        int        fid;
+        Fid        *next;
+        Mailbox        *mb;
+        Message        *m;
+        Message *mtop;                // top level message
+
+        //finger pointers to speed up reads of large directories
+        long        foff;        // offset/DIRLEN of finger
+        Message        *fptr;        // pointer to message at off
+        int        fvers;        // mailbox version when finger was saved
+};
+
+ulong        path;                // incremented for each new file
+Fid        *fids;
+int        mfd[2];
+char        user[Elemlen];
+int        messagesize = 4*1024*IOHDRSZ;
+uchar        mdata[8*1024*IOHDRSZ];
+uchar        mbuf[8*1024*IOHDRSZ];
+Fcall        thdr;
+Fcall        rhdr;
+int        fflg;
+char        *mntpt;
+int        biffing;
+int        plumbing = 1;
+
+QLock        mbllock;
+Mailbox        *mbl;
+
+Fid                *newfid(int);
+void                error(char*);
+void                io(void);
+void                *erealloc(void*, ulong);
+void                *emalloc(ulong);
+void                usage(void);
+void                run_io(void*);
+void                reader(void*);
+int                readheader(Message*, char*, int, int);
+int                cistrncmp(char*, char*, int);
+int                tokenconvert(String*, char*, int);
+String*                stringconvert(String*, char*, int);
+void                post(char*, char*, int);
+
+char        *rflush(Fid*), *rauth(Fid*),
+        *rattach(Fid*), *rwalk(Fid*),
+        *ropen(Fid*), *rcreate(Fid*),
+        *rread(Fid*), *rwrite(Fid*), *rclunk(Fid*),
+        *rremove(Fid*), *rstat(Fid*), *rwstat(Fid*),
+        *rversion(Fid*);
+
+char         *(*fcalls[])(Fid*) = {
+        [Tflush]        rflush,
+        [Tversion]        rversion,
+        [Tauth]        rauth,
+        [Tattach]        rattach,
+        [Twalk]                rwalk,
+        [Topen]                ropen,
+        [Tcreate]        rcreate,
+        [Tread]                rread,
+        [Twrite]        rwrite,
+        [Tclunk]        rclunk,
+        [Tremove]        rremove,
+        [Tstat]                rstat,
+        [Twstat]        rwstat,
+};
+
+char        Eperm[] =        "permission denied";
+char        Enotdir[] =        "not a directory";
+char        Enoauth[] =        "upas/fs: authentication not required";
+char        Enotexist[] =        "file does not exist";
+char        Einuse[] =        "file in use";
+char        Eexist[] =        "file exists";
+char        Enotowner[] =        "not owner";
+char        Eisopen[] =         "file already open for I/O";
+char        Excl[] =         "exclusive use file already open";
+char        Ename[] =         "illegal name";
+char        Ebadctl[] =        "unknown control message";
+
+char *dirtab[] =
+{
+[Qdir]                ".",
+[Qbody]                "body",
+[Qbcc]                "bcc",
+[Qcc]                "cc",
+[Qdate]                "date",
+[Qdigest]        "digest",
+[Qdisposition]        "disposition",
+[Qfilename]        "filename",
+[Qfrom]                "from",
+[Qheader]        "header",
+[Qinfo]                "info",
+[Qinreplyto]        "inreplyto",
+[Qlines]        "lines",
+[Qmimeheader]        "mimeheader",
+[Qmessageid]        "messageid",
+[Qraw]                "raw",
+[Qrawunix]        "rawunix",
+[Qrawbody]        "rawbody",
+[Qrawheader]        "rawheader",
+[Qreplyto]        "replyto",
+[Qsender]        "sender",
+[Qsubject]        "subject",
+[Qto]                "to",
+[Qtype]                "type",
+[Qunixdate]        "unixdate",
+[Qunixheader]        "unixheader",
+[Qctl]                "ctl",
+[Qmboxctl]        "ctl",
+};
+
+enum
+{
+        Hsize=        1277,
+};
+
+Hash        *htab[Hsize];
+
+int        debug;
+int        fflag;
+int        logging;
+
+void
+usage(void)
+{
+        fprint(2, "usage: %s [-b -m mountpoint]\n", argv0);
+        threadexits("usage");
+}
+
+void
+notifyf(void *a, char *s)
+{
+        USED(a);
+        if(strncmp(s, "interrupt", 9) == 0)
+                noted(NCONT);
+        noted(NDFLT);
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+        int p[2], std, nodflt;
+        char maildir[128];
+        char mbox[128];
+        char *mboxfile, *err;
+        char srvfile[64];
+        int srvpost;
+
+        rfork(RFNOTEG);
+        mntpt = nil;
+        fflag = 0;
+        mboxfile = nil;
+        std = 0;
+        nodflt = 0;
+        srvpost = 0;
+
+        ARGBEGIN{
+        case 'b':
+                biffing = 1;
+                break;
+        case 'f':
+                fflag = 1;
+                mboxfile = ARGF();
+                break;
+        case 'm':
+                mntpt = ARGF();
+                break;
+        case 'd':
+                debug = 1;
+                break;
+        case 'p':
+                plumbing = 0;
+                break;
+        case 's':
+                srvpost = 1;
+                break;
+        case 'l':
+                logging = 1;
+                break;
+        case 'n':
+                nodflt = 1;
+                break;
+        default:
+                usage();
+        }ARGEND
+
+        if(pipe(p) < 0)
+                error("pipe failed");
+        mfd[0] = p[0];
+        mfd[1] = p[0];
+
+        notify(notifyf);
+        strcpy(user, getuser());
+        if(mntpt == nil){
+                snprint(maildir, sizeof(maildir), "/mail/fs");
+                mntpt = maildir;
+        }
+        if(mboxfile == nil && !nodflt){
+                snprint(mbox, sizeof(mbox), "/mail/box/%s/mbox", user);
+                mboxfile = mbox;
+                std = 1;
+        }
+
+        if(debug)
+                fmtinstall('F', fcallfmt);
+
+        if(mboxfile != nil){
+                err = newmbox(mboxfile, "mbox", std);
+                if(err != nil)
+                        sysfatal("opening mailbox: %s", err);
+        }
+
+        switch(rfork(RFFDG|RFPROC|RFNAMEG|RFNOTEG)){ /* jpc removed RFEND */
+        case -1:
+                error("fork");
+        case 0:
+                henter(PATH(0, Qtop), dirtab[Qctl],
+                        (Qid){PATH(0, Qctl), 0, QTFILE}, nil, nil);
+                close(p[1]);
+                io();
+                postnote(PNGROUP, getpid(), "die yankee pig dog");
+                break;
+        default:
+                close(p[0]);        /* don't deadlock if child fails */
+                if(srvpost){
+                        sprint(srvfile, "/srv/upasfs.%s", user);
+                        /* post(srvfile, "upasfs", p[1]);  jpc */
+                        post9pservice(p[1], "upasfs");   /* jpc */
+                } else {
+                        error("tried to mount, fixme");     /* jpc */
+                        /* if(mount(p[1], -1, mntpt, MREPL, "") < 0)
+                                error("mount failed");   jpc */
+                }
+        }
+        threadexits(0);
+}
+
+void run_io(void *v) {
+        int *p;
+
+        p =  v;
+        henter(PATH(0, Qtop), dirtab[Qctl],
+                (Qid){PATH(0, Qctl), 0, QTFILE}, nil, nil);
+        close(p[1]);
+        io();
+        postnote(PNGROUP, getpid(), "die yankee pig dog");
+}
+
+static int
+fileinfo(Message *m, int t, char **pp)
+{
+        char *p;
+        int len;
+
+        p = "";
+        len = 0;
+        switch(t){
+        case Qbody:
+                p = m->body;
+                len = m->bend - m->body;
+                break;
+        case Qbcc:
+                if(m->bcc822){
+                        p = s_to_c(m->bcc822);
+                        len = strlen(p);
+                }
+                break;
+        case Qcc:
+                if(m->cc822){
+                        p = s_to_c(m->cc822);
+                        len = strlen(p);
+                }
+                break;
+        case Qdisposition:
+                switch(m->disposition){
+                case Dinline:
+                        p = "inline";
+                        break;
+                case Dfile:
+                        p = "file";
+                        break;
+                }
+                len = strlen(p);
+                break;
+        case Qdate:
+                if(m->date822){
+                        p = s_to_c(m->date822);
+                        len = strlen(p);
+                } else if(m->unixdate != nil){
+                        p = s_to_c(m->unixdate);
+                        len = strlen(p);
+                }
+                break;
+        case Qfilename:
+                if(m->filename){
+                        p = s_to_c(m->filename);
+                        len = strlen(p);
+                }
+                break;
+        case Qinreplyto:
+                if(m->inreplyto822){
+                        p = s_to_c(m->inreplyto822);
+                        len = strlen(p);
+                }
+                break;
+        case Qmessageid:
+                if(m->messageid822){
+                        p = s_to_c(m->messageid822);
+                        len = strlen(p);
+                }
+                break;
+        case Qfrom:
+                if(m->from822){
+                        p = s_to_c(m->from822);
+                        len = strlen(p);
+                } else if(m->unixfrom != nil){
+                        p = s_to_c(m->unixfrom);
+                        len = strlen(p);
+                }
+                break;
+        case Qheader:
+                p = m->header;
+                len = headerlen(m);
+                break;
+        case Qlines:
+                p = m->lines;
+                if(*p == 0)
+                        countlines(m);
+                len = strlen(m->lines);
+                break;
+        case Qraw:
+                p = m->start;
+                if(strncmp(m->start, "From ", 5) == 0){
+                        p = strchr(p, '\n');
+                        if(p == nil)
+                                p = m->start;
+                        else
+                                p++;
+                }
+                len = m->end - p;
+                break;
+        case Qrawunix:
+                p = m->start;
+                len = m->end - p;
+                break;
+        case Qrawbody:
+                p = m->rbody;
+                len = m->rbend - p;
+                break;
+        case Qrawheader:
+                p = m->header;
+                len = m->hend - p;
+                break;
+        case Qmimeheader:
+                p = m->mheader;
+                len = m->mhend - p;
+                break;
+        case Qreplyto:
+                p = nil;
+                if(m->replyto822 != nil){
+                        p = s_to_c(m->replyto822);
+                        len = strlen(p);
+                } else if(m->from822 != nil){
+                        p = s_to_c(m->from822);
+                        len = strlen(p);
+                } else if(m->sender822 != nil){
+                        p = s_to_c(m->sender822);
+                        len = strlen(p);
+                } else if(m->unixfrom != nil){
+                        p = s_to_c(m->unixfrom);
+                        len = strlen(p);
+                }
+                break;
+        case Qsender:
+                if(m->sender822){
+                        p = s_to_c(m->sender822);
+                        len = strlen(p);
+                }
+                break;
+        case Qsubject:
+                p = nil;
+                if(m->subject822){
+                        p = s_to_c(m->subject822);
+                        len = strlen(p);
+                }
+                break;
+        case Qto:
+                if(m->to822){
+                        p = s_to_c(m->to822);
+                        len = strlen(p);
+                }
+                break;
+        case Qtype:
+                if(m->type){
+                        p = s_to_c(m->type);
+                        len = strlen(p);
+                }
+                break;
+        case Qunixdate:
+                if(m->unixdate){
+                        p = s_to_c(m->unixdate);
+                        len = strlen(p);
+                }
+                break;
+        case Qunixheader:
+                if(m->unixheader){
+                        p = s_to_c(m->unixheader);
+                        len = s_len(m->unixheader);
+                }
+                break;
+        case Qdigest:
+                if(m->sdigest){
+                        p = s_to_c(m->sdigest);
+                        len = strlen(p);
+                }
+                break;
+        }
+        *pp = p;
+        return len;
+}
+
+int infofields[] = {
+        Qfrom,
+        Qto,
+        Qcc,
+        Qreplyto,
+        Qunixdate,
+        Qsubject,
+        Qtype,
+        Qdisposition,
+        Qfilename,
+        Qdigest,
+        Qbcc,
+        Qinreplyto,
+        Qdate,
+        Qsender,
+        Qmessageid,
+        Qlines,
+        -1,
+};
+
+static int
+readinfo(Message *m, char *buf, long off, int count)
+{
+        char *p;
+        int len, i, n;
+        String *s;
+
+        s = s_new();
+        len = 0;
+        for(i = 0; len < count && infofields[i] >= 0; i++){
+                n = fileinfo(m, infofields[i], &p);
+                s = stringconvert(s, p, n);
+                s_append(s, "\n");
+                p = s_to_c(s);
+                n = strlen(p);
+                if(off > 0){
+                        if(off >= n){
+                                off -= n;
+                                continue;
+                        }
+                        p += off;
+                        n -= off;
+                        off = 0;
+                }
+                if(n > count - len)
+                        n = count - len;
+                if(buf)
+                        memmove(buf+len, p, n);
+                len += n;
+        }
+        s_free(s);
+        return len;
+}
+
+static void
+mkstat(Dir *d, Mailbox *mb, Message *m, int t)
+{
+        char *p;
+
+        d->uid = user;
+        d->gid = user;
+        d->muid = user;
+        d->mode = 0444;
+        d->qid.vers = 0;
+        d->qid.type = QTFILE;
+        d->type = 0;
+        d->dev = 0;
+        if(mb != nil && mb->d != nil){
+                d->atime = mb->d->atime;
+                d->mtime = mb->d->mtime;
+        } else {
+                d->atime = time(0);
+                d->mtime = d->atime;
+        }
+
+        switch(t){
+        case Qtop:
+                d->name = ".";
+                d->mode = DMDIR|0555;
+                d->atime = d->mtime = time(0);
+                d->length = 0;
+                d->qid.path = PATH(0, Qtop);
+                d->qid.type = QTDIR;
+                break;
+        case Qmbox:
+                d->name = mb->name;
+                d->mode = DMDIR|0555;
+                d->length = 0;
+                d->qid.path = PATH(mb->id, Qmbox);
+                d->qid.type = QTDIR;
+                d->qid.vers = mb->vers;
+                break;
+        case Qdir:
+                d->name = m->name;
+                d->mode = DMDIR|0555;
+                d->length = 0;
+                d->qid.path = PATH(m->id, Qdir);
+                d->qid.type = QTDIR;
+                break;
+        case Qctl:
+                d->name = dirtab[t];
+                d->mode = 0666;
+                d->atime = d->mtime = time(0);
+                d->length = 0;
+                d->qid.path = PATH(0, Qctl);
+                break;
+        case Qmboxctl:
+                d->name = dirtab[t];
+                d->mode = 0222;
+                d->atime = d->mtime = time(0);
+                d->length = 0;
+                d->qid.path = PATH(mb->id, Qmboxctl);
+                break;
+        case Qinfo:
+                d->name = dirtab[t];
+                d->length = readinfo(m, nil, 0, 1<<30);
+                d->qid.path = PATH(m->id, t);
+                break;
+        default:
+                d->name = dirtab[t];
+                d->length = fileinfo(m, t, &p);
+                d->qid.path = PATH(m->id, t);
+                break;
+        }
+}
+
+char*
+rversion(Fid* dummy)
+{
+        Fid *f;
+
+        if(thdr.msize < 256)
+                return "max messagesize too small";
+        if(thdr.msize < messagesize)
+                messagesize = thdr.msize;
+        rhdr.msize = messagesize;
+        if(strncmp(thdr.version, "9P2000", 6) != 0)
+                return "unknown 9P version";
+        else
+                rhdr.version = "9P2000";
+        for(f = fids; f; f = f->next)
+                if(f->busy)
+                        rclunk(f);
+        return nil;
+}
+
+char*
+rauth(Fid* dummy)
+{
+        return Enoauth;
+}
+
+char*
+rflush(Fid *f)
+{
+        USED(f);
+        return 0;
+}
+
+char*
+rattach(Fid *f)
+{
+        f->busy = 1;
+        f->m = nil;
+        f->mb = nil;
+        f->qid.path = PATH(0, Qtop);
+        f->qid.type = QTDIR;
+        f->qid.vers = 0;
+        rhdr.qid = f->qid;
+        if(strcmp(thdr.uname, user) != 0)
+                return Eperm;
+        return 0;
+}
+
+static Fid*
+doclone(Fid *f, int nfid)
+{
+        Fid *nf;
+
+        nf = newfid(nfid);
+        if(nf->busy)
+                return nil;
+        nf->busy = 1;
+        nf->open = 0;
+        nf->m = f->m;
+        nf->mtop = f->mtop;
+        nf->mb = f->mb;
+        if(f->mb != nil)
+                mboxincref(f->mb);
+        if(f->mtop != nil){
+                qlock(&f->mb->ql);
+                msgincref(f->mtop);
+                qunlock(&f->mb->ql);
+        }
+        nf->qid = f->qid;
+        return nf;
+}
+
+char*
+dowalk(Fid *f, char *name)
+{
+        int t;
+        Mailbox *omb, *mb;
+        char *rv, *p;
+        Hash *h;
+
+        t = FILE(f->qid.path);
+
+        rv = Enotexist;
+
+        omb = f->mb;
+        if(omb)
+                qlock(&omb->ql);
+        else
+                qlock(&mbllock);
+
+        // this must catch everything except . and ..
+retry:
+        h = hlook(f->qid.path, name);
+        if(h != nil){
+                f->mb = h->mb;
+                f->m = h->m;
+                switch(t){
+                case Qtop:
+                        if(f->mb != nil)
+                                mboxincref(f->mb);
+                        break;
+                case Qmbox:
+                        if(f->m){
+                                msgincref(f->m);
+                                f->mtop = f->m;
+                        }
+                        break;
+                }
+                f->qid = h->qid;
+                rv = nil;
+        } else if((p = strchr(name, '.')) != nil && *name != '.'){
+                *p = 0;
+                goto retry;
+        }
+
+        if(omb)
+                qunlock(&omb->ql);
+        else
+                qunlock(&mbllock);
+        if(rv == nil)
+                return rv;
+
+        if(strcmp(name, ".") == 0)
+                return nil;
+
+        if(f->qid.type != QTDIR)
+                return Enotdir;
+
+        if(strcmp(name, "..") == 0){
+                switch(t){
+                case Qtop:
+                        f->qid.path = PATH(0, Qtop);
+                        f->qid.type = QTDIR;
+                        f->qid.vers = 0;
+                        break;
+                case Qmbox:
+                        f->qid.path = PATH(0, Qtop);
+                        f->qid.type = QTDIR;
+                        f->qid.vers = 0;
+                        qlock(&mbllock);
+                        mb = f->mb;
+                        f->mb = nil;
+                        mboxdecref(mb);
+                        qunlock(&mbllock);
+                        break;
+                case Qdir:
+                        qlock(&f->mb->ql);
+                        if(f->m->whole == f->mb->root){
+                                f->qid.path = PATH(f->mb->id, Qmbox);
+                                f->qid.type = QTDIR;
+                                f->qid.vers = f->mb->d->qid.vers;
+                                msgdecref(f->mb, f->mtop);
+                                f->m = f->mtop = nil;
+                        } else {
+                                f->m = f->m->whole;
+                                f->qid.path = PATH(f->m->id, Qdir);
+                                f->qid.type = QTDIR;
+                        }
+                        qunlock(&f->mb->ql);
+                        break;
+                }
+                rv = nil;
+        }
+        return rv;
+}
+
+char*
+rwalk(Fid *f)
+{
+        Fid *nf;
+        char *rv;
+        int i;
+
+        if(f->open)
+                return Eisopen;
+
+        rhdr.nwqid = 0;
+        nf = nil;
+
+        /* clone if requested */
+        if(thdr.newfid != thdr.fid){
+                nf = doclone(f, thdr.newfid);
+                if(nf == nil)
+                        return "new fid in use";
+                f = nf;
+        }
+
+        /* if it's just a clone, return */
+        if(thdr.nwname == 0 && nf != nil)
+                return nil;
+
+        /* walk each element */
+        rv = nil;
+        for(i = 0; i < thdr.nwname; i++){
+                rv = dowalk(f, thdr.wname[i]);
+                if(rv != nil){
+                        if(nf != nil)        
+                                rclunk(nf);
+                        break;
+                }
+                rhdr.wqid[i] = f->qid;
+        }
+        rhdr.nwqid = i;
+
+        /* we only error out if no walk  */
+        if(i > 0)
+                rv = nil;
+
+        return rv;
+}
+
+char *
+ropen(Fid *f)
+{
+        int file;
+
+        if(f->open)
+                return Eisopen;
+
+        file = FILE(f->qid.path);
+        if(thdr.mode != OREAD)
+                if(file != Qctl && file != Qmboxctl)
+                        return Eperm;
+
+        // make sure we've decoded
+        if(file == Qbody){
+                if(f->m->decoded == 0)
+                        decode(f->m);
+                if(f->m->converted == 0)
+                        convert(f->m);
+        }
+
+        rhdr.iounit = 0;
+        rhdr.qid = f->qid;
+        f->open = 1;
+        return 0;
+}
+
+char *
+rcreate(Fid* dummy)
+{
+        return Eperm;
+}
+
+int
+readtopdir(Fid* dummy, uchar *buf, long off, int cnt, int blen)
+{
+        Dir d;
+        int m, n;
+        long pos;
+        Mailbox *mb;
+
+        n = 0;
+        pos = 0;
+        mkstat(&d, nil, nil, Qctl);
+        m = convD2M(&d, &buf[n], blen);
+        if(off <= pos){
+                if(m <= BIT16SZ || m > cnt)
+                        return 0;
+                n += m;
+                cnt -= m;
+        }
+        pos += m;
+                
+        for(mb = mbl; mb != nil; mb = mb->next){
+                mkstat(&d, mb, nil, Qmbox);
+                m = convD2M(&d, &buf[n], blen-n);
+                if(off <= pos){
+                        if(m <= BIT16SZ || m > cnt)
+                                break;
+                        n += m;
+                        cnt -= m;
+                }
+                pos += m;
+        }
+        return n;
+}
+
+int
+readmboxdir(Fid *f, uchar *buf, long off, int cnt, int blen)
+{
+        Dir d;
+        int n, m;
+        long pos;
+        Message *msg;
+
+        n = 0;
+        if(f->mb->ctl){
+                mkstat(&d, f->mb, nil, Qmboxctl);
+                m = convD2M(&d, &buf[n], blen);
+                if(off == 0){
+                        if(m <= BIT16SZ || m > cnt){
+                                f->fptr = nil;
+                                return 0;
+                        }
+                        n += m;
+                        cnt -= m;
+                } else
+                        off -= m;
+        }
+
+        // to avoid n**2 reads of the directory, use a saved finger pointer
+        if(f->mb->vers == f->fvers && off >= f->foff && f->fptr != nil){
+                msg = f->fptr;
+                pos = f->foff;
+        } else {
+                msg = f->mb->root->part;
+                pos = 0;
+        } 
+
+        for(; cnt > 0 && msg != nil; msg = msg->next){
+                // act like deleted files aren't there
+                if(msg->deleted)
+                        continue;
+
+                mkstat(&d, f->mb, msg, Qdir);
+                m = convD2M(&d, &buf[n], blen-n);
+                if(off <= pos){
+                        if(m <= BIT16SZ || m > cnt)
+                                break;
+                        n += m;
+                        cnt -= m;
+                }
+                pos += m;
+        }
+
+        // save a finger pointer for next read of the mbox directory
+        f->foff = pos;
+        f->fptr = msg;
+        f->fvers = f->mb->vers;
+
+        return n;
+}
+
+int
+readmsgdir(Fid *f, uchar *buf, long off, int cnt, int blen)
+{
+        Dir d;
+        int i, n, m;
+        long pos;
+        Message *msg;
+
+        n = 0;
+        pos = 0;
+        for(i = 0; i < Qmax; i++){
+                mkstat(&d, f->mb, f->m, i);
+                m = convD2M(&d, &buf[n], blen-n);
+                if(off <= pos){
+                        if(m <= BIT16SZ || m > cnt)
+                                return n;
+                        n += m;
+                        cnt -= m;
+                }
+                pos += m;
+        }
+        for(msg = f->m->part; msg != nil; msg = msg->next){
+                mkstat(&d, f->mb, msg, Qdir);
+                m = convD2M(&d, &buf[n], blen-n);
+                if(off <= pos){
+                        if(m <= BIT16SZ || m > cnt)
+                                break;
+                        n += m;
+                        cnt -= m;
+                }
+                pos += m;
+        }
+
+        return n;
+}
+
+char*
+rread(Fid *f)
+{
+        long off;
+        int t, i, n, cnt;
+        char *p;
+
+        rhdr.count = 0;
+        off = thdr.offset;
+        cnt = thdr.count;
+
+        if(cnt > messagesize - IOHDRSZ)
+                cnt = messagesize - IOHDRSZ;
+
+        rhdr.data = (char*)mbuf;
+
+        t = FILE(f->qid.path);
+        if(f->qid.type & QTDIR){
+                if(t == Qtop) {
+                        qlock(&mbllock);
+                        n = readtopdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+                        qunlock(&mbllock);
+                } else if(t == Qmbox) {
+                        qlock(&f->mb->ql);
+                        if(off == 0)
+                                syncmbox(f->mb, 1);
+                        n = readmboxdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+                        qunlock(&f->mb->ql);
+                } else if(t == Qmboxctl) {
+                        n = 0;
+                } else {
+                        n = readmsgdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+                }
+
+                rhdr.count = n;
+                return nil;
+        }
+
+        if(FILE(f->qid.path) == Qheader){
+                rhdr.count = readheader(f->m, (char*)mbuf, off, cnt);
+                return nil;
+        }
+
+        if(FILE(f->qid.path) == Qinfo){
+                rhdr.count = readinfo(f->m, (char*)mbuf, off, cnt);
+                return nil;
+        }
+
+        i = fileinfo(f->m, FILE(f->qid.path), &p);
+        if(off < i){
+                if((off + cnt) > i)
+                        cnt = i - off;
+                memmove(mbuf, p + off, cnt);
+                rhdr.count = cnt;
+        }
+        return nil;
+}
+
+char*
+rwrite(Fid *f)
+{
+        char *err;
+        char *token[1024];
+        int t, n;
+        String *file;
+
+        t = FILE(f->qid.path);
+        rhdr.count = thdr.count;
+        switch(t){
+        case Qctl:
+                if(thdr.count == 0)
+                        return Ebadctl;
+                if(thdr.data[thdr.count-1] == '\n')
+                        thdr.data[thdr.count-1] = 0;
+                else
+                        thdr.data[thdr.count] = 0;
+                n = tokenize(thdr.data, token, nelem(token));
+                if(n == 0)
+                        return Ebadctl;
+                if(strcmp(token[0], "open") == 0){
+                        file = s_new();
+                        switch(n){
+                        case 1:
+                                err = Ebadctl;
+                                break;
+                        case 2:
+                                mboxpath(token[1], getlog(), file, 0);
+                                err = newmbox(s_to_c(file), nil, 0);
+                                break;
+                        default:
+                                mboxpath(token[1], getlog(), file, 0);
+                                if(strchr(token[2], '/') != nil)
+                                        err = "/ not allowed in mailbox name";
+                                else
+                                        err = newmbox(s_to_c(file), token[2], 0);
+                                break;
+                        }
+                        s_free(file);
+                        return err;
+                }
+                if(strcmp(token[0], "close") == 0){
+                        if(n < 2)
+                                return nil;
+                        freembox(token[1]);
+                        return nil;
+                }
+                if(strcmp(token[0], "delete") == 0){
+                        if(n < 3)
+                                return nil;
+                        delmessages(n-1, &token[1]);
+                        return nil;
+                }
+                return Ebadctl;
+        case Qmboxctl:
+                if(f->mb && f->mb->ctl){
+                        if(thdr.count == 0)
+                                return Ebadctl;
+                        if(thdr.data[thdr.count-1] == '\n')
+                                thdr.data[thdr.count-1] = 0;
+                        else
+                                thdr.data[thdr.count] = 0;
+                        n = tokenize(thdr.data, token, nelem(token));
+                        if(n == 0)
+                                return Ebadctl;
+                        return (*f->mb->ctl)(f->mb, n, token);
+                }
+        }
+        return Eperm;
+}
+
+char *
+rclunk(Fid *f)
+{
+        Mailbox *mb;
+
+        f->busy = 0;
+        f->open = 0;
+        if(f->mtop != nil){
+                qlock(&f->mb->ql);
+                msgdecref(f->mb, f->mtop);
+                qunlock(&f->mb->ql);
+        }
+        f->m = f->mtop = nil;
+        mb = f->mb;
+        if(mb != nil){
+                f->mb = nil;
+                assert(mb->refs > 0);
+                qlock(&mbllock);
+                mboxdecref(mb);
+                qunlock(&mbllock);
+        }
+        f->fid = -1;
+        return 0;
+}
+
+char *
+rremove(Fid *f)
+{
+        if(f->m != nil){
+                if(f->m->deleted == 0)
+                        mailplumb(f->mb, f->m, 1);
+                f->m->deleted = 1;
+        }
+        return rclunk(f);
+}
+
+char *
+rstat(Fid *f)
+{
+        Dir d;
+
+        if(FILE(f->qid.path) == Qmbox){
+                qlock(&f->mb->ql);
+                syncmbox(f->mb, 1);
+                qunlock(&f->mb->ql);
+        }
+        mkstat(&d, f->mb, f->m, FILE(f->qid.path));
+        rhdr.nstat = convD2M(&d, mbuf, messagesize - IOHDRSZ);
+        rhdr.stat = mbuf;
+        return 0;
+}
+
+char *
+rwstat(Fid* dummy)
+{
+        return Eperm;
+}
+
+Fid *
+newfid(int fid)
+{
+        Fid *f, *ff;
+
+        ff = 0;
+        for(f = fids; f; f = f->next)
+                if(f->fid == fid)
+                        return f;
+                else if(!ff && !f->busy)
+                        ff = f;
+        if(ff){
+                ff->fid = fid;
+                ff->fptr = nil;
+                return ff;
+        }
+        f = emalloc(sizeof *f);
+        f->fid = fid;
+        f->fptr = nil;
+        f->next = fids;
+        fids = f;
+        return f;
+}
+
+int
+fidmboxrefs(Mailbox *mb)
+{
+        Fid *f;
+        int refs = 0;
+
+        for(f = fids; f; f = f->next){
+                if(f->mb == mb)
+                        refs++;
+        }
+        return refs;
+}
+
+void
+io(void)
+{
+        char *err;
+        int n, nw;
+
+        /* start a process to watch the mailboxes*/
+        if(plumbing){
+                proccreate(reader, nil, 16000);
+#if 0 /* jpc */
+                switch(rfork(RFPROC|RFMEM)){
+                case -1:
+                        /* oh well */
+                        break;
+                case 0:
+                        reader();
+                        threadexits(nil);
+                default:
+                        break;
+                }
+#endif /* jpc */
+        }
+
+        for(;;){
+                /*
+                 * reading from a pipe or a network device
+                 * will give an error after a few eof reads
+                 * however, we cannot tell the difference
+                 * between a zero-length read and an interrupt
+                 * on the processes writing to us,
+                 * so we wait for the error
+                 */
+                checkmboxrefs();
+                n = read9pmsg(mfd[0], mdata, messagesize);
+                if(n == 0)
+                        continue;
+                if(n < 0)
+                        return;
+                if(convM2S(mdata, n, &thdr) == 0)
+                        continue;
+
+                if(debug)
+                        fprint(2, "%s:<-%F\n", argv0, &thdr);
+
+                rhdr.data = (char*)mdata + messagesize;
+                if(!fcalls[thdr.type])
+                        err = "bad fcall type";
+                else
+                        err = (*fcalls[thdr.type])(newfid(thdr.fid));
+                if(err){
+                        rhdr.type = Rerror;
+                        rhdr.ename = err;
+                }else{
+                        rhdr.type = thdr.type + 1;
+                        rhdr.fid = thdr.fid;
+                }
+                rhdr.tag = thdr.tag;
+                if(debug)
+                        fprint(2, "%s:->%F\n", argv0, &rhdr);/**/
+                n = convS2M(&rhdr, mdata, messagesize);
+                if((nw = write(mfd[1], mdata, n)) != n) {
+                        fprint(2,"wrote %d bytes\n",nw);
+                        error("mount write");
+                }
+        }
+}
+
+void
+reader(void *dummy)
+{
+        ulong t;
+        Dir *d;
+        Mailbox *mb;
+
+        sleep(15*1000);
+        for(;;){
+                t = time(0);
+                qlock(&mbllock);
+                for(mb = mbl; mb != nil; mb = mb->next){
+                        assert(mb->refs > 0);
+                        if(mb->waketime != 0 && t > mb->waketime){
+                                qlock(&mb->ql);
+                                mb->waketime = 0;
+                                break;
+                        }
+
+                        d = dirstat(mb->path);
+                        if(d == nil)
+                                continue;
+
+                        qlock(&mb->ql);
+                        if(mb->d)
+                        if(d->qid.path != mb->d->qid.path
+                           || d->qid.vers != mb->d->qid.vers){
+                                free(d);
+                                break;
+                        }
+                        qunlock(&mb->ql);
+                        free(d);
+                }
+                qunlock(&mbllock);
+                if(mb != nil){
+                        syncmbox(mb, 1);
+                        qunlock(&mb->ql);
+                } else
+                        sleep(15*1000);
+        }
+}
+
+int
+newid(void)
+{
+        int rv;
+        static int id;
+        static Lock idlock;
+
+        lock(&idlock);
+        rv = ++id;
+        unlock(&idlock);
+
+        return rv;
+}
+
+void
+error(char *s)
+{
+        postnote(PNGROUP, getpid(), "die yankee pig dog");
+        fprint(2, "%s: %s: %r\n", argv0, s);
+        threadexits(s);
+}
+
+
+typedef struct Ignorance Ignorance;
+struct Ignorance
+{
+        Ignorance *next;
+        char        *str;                /* string */
+        int        partial;        /* true if not exact match */
+};
+Ignorance *ignorance;
+
+/*
+ *  read the file of headers to ignore 
+ */
+void
+readignore(void)
+{
+        char *p;
+        Ignorance *i;
+        Biobuf *b;
+
+        if(ignorance != nil)
+                return;
+
+        b = Bopen("/mail/lib/ignore", OREAD);
+        if(b == 0)
+                return;
+        while(p = Brdline(b, '\n')){
+                p[Blinelen(b)-1] = 0;
+                while(*p && (*p == ' ' || *p == '\t'))
+                        p++;
+                if(*p == '#')
+                        continue;
+                i = malloc(sizeof(Ignorance));
+                if(i == 0)
+                        break;
+                i->partial = strlen(p);
+                i->str = strdup(p);
+                if(i->str == 0){
+                        free(i);
+                        break;
+                }
+                i->next = ignorance;
+                ignorance = i;
+        }
+        Bterm(b);
+}
+
+int
+ignore(char *p)
+{
+        Ignorance *i;
+
+        readignore();
+        for(i = ignorance; i != nil; i = i->next)
+                if(cistrncmp(i->str, p, i->partial) == 0)
+                        return 1;
+        return 0;
+}
+
+int
+hdrlen(char *p, char *e)
+{
+        char *ep;
+
+        ep = p;
+        do {
+                ep = strchr(ep, '\n');
+                if(ep == nil){
+                        ep = e;
+                        break;
+                }
+                ep++;
+                if(ep >= e){
+                        ep = e;
+                        break;
+                }
+        } while(*ep == ' ' || *ep == '\t');
+        return ep - p;
+}
+
+// rfc2047 non-ascii
+typedef struct Charset Charset;
+struct Charset {
+        char *name;
+        int len;
+        int convert;
+        char *tcsname;
+} charsets[] =
+{
+        { "us-ascii",                8,        1, nil, },
+        { "utf-8",                5,        0, nil, },
+        { "iso-8859-1",                10,        1, nil, },
+        { "iso-8859-2",                10,        2, "8859-2", },
+        { "big5",                4,        2, "big5", },
+        { "iso-2022-jp",        11, 2, "jis", },
+        { "windows-1251",        12,        2, "cp1251"},
+        { "koi8-r",                6,        2, "koi8"},
+};
+
+int
+rfc2047convert(String *s, char *token, int len)
+{
+        char decoded[1024];
+        char utfbuf[2*1024];
+        int i;
+        char *e, *x;
+
+        if(len == 0)
+                return -1;
+
+        e = token+len-2;
+        token += 2;
+
+        // bail if we don't understand the character set
+        for(i = 0; i < nelem(charsets); i++)
+                if(cistrncmp(charsets[i].name, token, charsets[i].len) == 0)
+                if(token[charsets[i].len] == '?'){
+                        token += charsets[i].len + 1;
+                        break;
+                }
+        if(i >= nelem(charsets))
+                return -1;
+
+        // bail if it doesn't fit 
+        if(e-token > sizeof(decoded)-1)
+                return -1;
+
+        // bail if we don't understand the encoding
+        if(cistrncmp(token, "b?", 2) == 0){
+                token += 2;
+                len = dec64((uchar*)decoded, sizeof(decoded), token, e-token);
+                decoded[len] = 0;
+        } else if(cistrncmp(token, "q?", 2) == 0){
+                token += 2;
+                len = decquoted(decoded, token, e);
+                if(len > 0 && decoded[len-1] == '\n')
+                        len--;
+                decoded[len] = 0;
+        } else
+                return -1;
+
+        switch(charsets[i].convert){
+        case 0:
+                s_append(s, decoded);
+                break;
+        case 1:
+                latin1toutf(utfbuf, decoded, decoded+len);
+                s_append(s, utfbuf);
+                break;
+        case 2:
+                if(xtoutf(charsets[i].tcsname, &x, decoded, decoded+len) <= 0){
+                        s_append(s, decoded);
+                } else {
+                        s_append(s, x);
+                        free(x);
+                }
+                break;
+        }
+
+        return 0;
+}
+
+char*
+rfc2047start(char *start, char *end)
+{
+        int quests;
+
+        if(*--end != '=')
+                return nil;
+        if(*--end != '?')
+                return nil;
+
+        quests = 0;
+        for(end--; end >= start; end--){
+                switch(*end){
+                case '=':
+                        if(quests == 3 && *(end+1) == '?')
+                                return end;
+                        break;
+                case '?':
+                        ++quests;
+                        break;
+                case ' ':
+                case '\t':
+                case '\n':
+                case '\r':
+                        /* can't have white space in a token */
+                        return nil;
+                }
+        }
+        return nil;
+}
+
+// convert a header line
+String*
+stringconvert(String *s, char *uneaten, int len)
+{
+        char *token;
+        char *p;
+        int i;
+
+        s = s_reset(s);
+        p = uneaten;
+        for(i = 0; i < len; i++){
+                if(*p++ == '='){
+                        token = rfc2047start(uneaten, p);
+                        if(token != nil){
+                                s_nappend(s, uneaten, token-uneaten);
+                                if(rfc2047convert(s, token, p - token) < 0)
+                                        s_nappend(s, token, p - token);
+                                uneaten = p;
+                        }
+                }
+        }
+        if(p > uneaten)
+                s_nappend(s, uneaten, p-uneaten);
+        return s;
+}
+
+int
+readheader(Message *m, char *buf, int off, int cnt)
+{
+        char *p, *e;
+        int n, ns;
+        char *to = buf;
+        String *s;
+
+        p = m->header;
+        e = m->hend;
+        s = nil;
+
+        // copy in good headers
+        while(cnt > 0 && p < e){
+                n = hdrlen(p, e);
+                if(ignore(p)){
+                        p += n;
+                        continue;
+                }
+
+                // rfc2047 processing
+                s = stringconvert(s, p, n);
+                ns = s_len(s);
+                if(off > 0){
+                        if(ns <= off){
+                                off -= ns;
+                                p += n;
+                                continue;
+                        }
+                        ns -= off;
+                }
+                if(ns > cnt)
+                        ns = cnt;
+                memmove(to, s_to_c(s)+off, ns);
+                to += ns;
+                p += n;
+                cnt -= ns;
+                off = 0;
+        }
+
+        s_free(s);
+        return to - buf;
+}
+
+int
+headerlen(Message *m)
+{
+        char buf[1024];
+        int i, n;
+
+        if(m->hlen >= 0)
+                return m->hlen;
+        for(n = 0; ; n += i){
+                i = readheader(m, buf, n, sizeof(buf));
+                if(i <= 0)
+                        break;
+        }
+        m->hlen = n;
+        return n;
+}
+
+QLock hashlock;
+
+uint
+hash(ulong ppath, char *name)
+{
+        uchar *p;
+        uint h;
+
+        h = 0;
+        for(p = (uchar*)name; *p; p++)
+                h = h*7 + *p;
+        h += ppath;
+
+        return h % Hsize;
+}
+
+Hash*
+hlook(ulong ppath, char *name)
+{
+        int h;
+        Hash *hp;
+
+        qlock(&hashlock);
+        h = hash(ppath, name);
+        for(hp = htab[h]; hp != nil; hp = hp->next)
+                if(ppath == hp->ppath && strcmp(name, hp->name) == 0){
+                        qunlock(&hashlock);
+                        return hp;
+                }
+        qunlock(&hashlock);
+        return nil;
+}
+
+void
+henter(ulong ppath, char *name, Qid qid, Message *m, Mailbox *mb)
+{
+        int h;
+        Hash *hp, **l;
+
+        qlock(&hashlock);
+        h = hash(ppath, name);
+        for(l = &htab[h]; *l != nil; l = &(*l)->next){
+                hp = *l;
+                if(ppath == hp->ppath && strcmp(name, hp->name) == 0){
+                        hp->m = m;
+                        hp->mb = mb;
+                        hp->qid = qid;
+                        qunlock(&hashlock);
+                        return;
+                }
+        }
+
+        *l = hp = emalloc(sizeof(*hp));
+        hp->m = m;
+        hp->mb = mb;
+        hp->qid = qid;
+        hp->name = name;
+        hp->ppath = ppath;
+        qunlock(&hashlock);
+}
+
+void
+hfree(ulong ppath, char *name)
+{
+        int h;
+        Hash *hp, **l;
+
+        qlock(&hashlock);
+        h = hash(ppath, name);
+        for(l = &htab[h]; *l != nil; l = &(*l)->next){
+                hp = *l;
+                if(ppath == hp->ppath && strcmp(name, hp->name) == 0){
+                        hp->mb = nil;
+                        *l = hp->next;
+                        free(hp);
+                        break;
+                }
+        }
+        qunlock(&hashlock);
+}
+
+int
+hashmboxrefs(Mailbox *mb)
+{
+        int h;
+        Hash *hp;
+        int refs = 0;
+
+        qlock(&hashlock);
+        for(h = 0; h < Hsize; h++){
+                for(hp = htab[h]; hp != nil; hp = hp->next)
+                        if(hp->mb == mb)
+                                refs++;
+        }
+        qunlock(&hashlock);
+        return refs;
+}
+
+void
+checkmboxrefs(void)
+{
+        int f, refs;
+        Mailbox *mb;
+
+        qlock(&mbllock);
+        for(mb=mbl; mb; mb=mb->next){
+                qlock(&mb->ql);
+                refs = (f=fidmboxrefs(mb))+1;
+                if(refs != mb->refs){
+                        fprint(2, "mbox %s %s ref mismatch actual %d (%d+1) expected %d\n", mb->name, mb->path, refs, f, mb->refs);
+                        abort();
+                }
+                qunlock(&mb->ql);
+        }
+        qunlock(&mbllock);
+}
+
+void
+post(char *name, char *envname, int srvfd)
+{
+        int fd;
+        char buf[32];
+
+        fd = create(name, OWRITE, 0600);
+        if(fd < 0)
+                error("post failed");
+        sprint(buf, "%d",srvfd);
+        if(write(fd, buf, strlen(buf)) != strlen(buf))
+                error("srv write");
+        close(fd);
+        putenv(envname, name);
+}
diff --git a/src/cmd/upas/fs/imap4.c b/src/cmd/upas/fs/imap4.c
t@@ -0,0 +1,876 @@
+#include "common.h"
+#include 
+#include 
+#include 
+#include 
+#include "dat.h"
+
+#pragma varargck argpos imap4cmd 2
+#pragma varargck        type        "Z"        char*
+
+int        doublequote(Fmt*);
+int        pipeline = 1;
+
+/* static char Eio[] = "i/o error"; jpc */
+
+typedef struct Imap Imap;
+struct Imap {
+        char *freep;        // free this to free the strings below
+
+        char *host;
+        char *user;
+        char *mbox;
+
+        int mustssl;
+        int refreshtime;
+        int debug;
+
+        ulong tag;
+        ulong validity;
+        int nmsg;
+        int size;
+        char *base;
+        char *data;
+
+        vlong *uid;
+        int nuid;
+        int muid;
+
+        Thumbprint *thumb;
+
+        // open network connection
+        Biobuf bin;
+        Biobuf bout;
+        int fd;
+};
+
+static char*
+removecr(char *s)
+{
+        char *r, *w;
+
+        for(r=w=s; *r; r++)
+                if(*r != '\r')
+                        *w++ = *r;
+        *w = '\0';
+        return s;
+}
+
+//
+// send imap4 command
+//
+static void
+imap4cmd(Imap *imap, char *fmt, ...)
+{
+        char buf[128], *p;
+        va_list va;
+
+        va_start(va, fmt);
+        p = buf+sprint(buf, "9X%lud ", imap->tag);
+        vseprint(p, buf+sizeof(buf), fmt, va);
+        va_end(va);
+
+        p = buf+strlen(buf);
+        if(p > (buf+sizeof(buf)-3))
+                sysfatal("imap4 command too long");
+
+        if(imap->debug)
+                fprint(2, "-> %s\n", buf);
+        strcpy(p, "\r\n");
+        Bwrite(&imap->bout, buf, strlen(buf));
+        Bflush(&imap->bout);
+}
+
+enum {
+        OK,
+        NO,
+        BAD,
+        BYE,
+        EXISTS,
+        STATUS,
+        FETCH,
+        UNKNOWN,
+};
+
+static char *verblist[] = {
+[OK]                "OK",
+[NO]                "NO",
+[BAD]        "BAD",
+[BYE]        "BYE",
+[EXISTS]        "EXISTS",
+[STATUS]        "STATUS",
+[FETCH]        "FETCH",
+};
+
+static int
+verbcode(char *verb)
+{
+        int i;
+        char *q;
+
+        if(q = strchr(verb, ' '))
+                *q = '\0';
+
+        for(i=0; idata == nil){
+                imap->base = emalloc(n+1);        
+                imap->data = imap->base;
+                imap->size = n+1;
+        }
+        if(n >= imap->size){
+                // friggin microsoft - reallocate
+                i = imap->data - imap->base;
+                imap->base = erealloc(imap->base, i+n+1);
+                imap->data = imap->base + i;
+                imap->size = n+1;
+        }
+}
+
+
+//
+// get imap4 response line.  there might be various 
+// data or other informational lines mixed in.
+//
+static char*
+imap4resp(Imap *imap)
+{
+        char *line, *p, *ep, *op, *q, *r, *en, *verb;
+        int i, n;
+        static char error[256];
+
+        while(p = Brdline(&imap->bin, '\n')){
+                ep = p+Blinelen(&imap->bin);
+                while(ep > p && (ep[-1]=='\n' || ep[-1]=='\r'))
+                        *--ep = '\0';
+                
+                if(imap->debug)
+                        fprint(2, "<- %s\n", p);
+                strupr(p);
+
+                switch(p[0]){
+                case '+':
+                        if(imap->tag == 0)
+                                fprint(2, "unexpected: %s\n", p);
+                        break;
+
+                // ``unsolicited'' information; everything happens here.
+                case '*':
+                        if(p[1]!=' ')
+                                continue;
+                        p += 2;
+                        line = p;
+                        n = strtol(p, &p, 10);
+                        if(*p==' ')
+                                p++;
+                        verb = p;
+                        
+                        if(p = strchr(verb, ' '))
+                                p++;
+                        else
+                                p = verb+strlen(verb);
+
+                        switch(verbcode(verb)){
+                        case OK:
+                        case NO:
+                        case BAD:
+                                // human readable text at p;
+                                break;
+                        case BYE:
+                                // early disconnect
+                                // human readable text at p;
+                                break;
+
+                        // * 32 EXISTS
+                        case EXISTS:
+                                imap->nmsg = n;
+                                break;
+
+                        // * STATUS Inbox (MESSAGES 2 UIDVALIDITY 960164964)
+                        case STATUS:
+                                if(q = strstr(p, "MESSAGES"))
+                                        imap->nmsg = atoi(q+8);
+                                if(q = strstr(p, "UIDVALIDITY"))
+                                        imap->validity = strtoul(q+11, 0, 10);
+                                break;
+
+                        case FETCH:
+                                // * 1 FETCH (uid 8889 RFC822.SIZE 3031 body[] {3031}
+                                // <3031 bytes of data>
+                                 // )
+                                if(strstr(p, "RFC822.SIZE") && strstr(p, "BODY[]")){
+                                        if((q = strchr(p, '{')) 
+                                        && (n=strtol(q+1, &en, 0), *en=='}')){
+                                                if(imap->data == nil || n >= imap->size)
+                                                        imapgrow(imap, n);
+                                                if((i = Bread(&imap->bin, imap->data, n)) != n){
+                                                        snprint(error, sizeof error,
+                                                                "short read %d != %d: %r\n",
+                                                                i, n);
+                                                        return error;
+                                                }
+                                                if(imap->debug)
+                                                        fprint(2, "<- read %d bytes\n", n);
+                                                imap->data[n] = '\0';
+                                                if(imap->debug)
+                                                        fprint(2, "<- %s\n", imap->data);
+                                                imap->data += n;
+                                                imap->size -= n;
+                                                p = Brdline(&imap->bin, '\n');
+                                                if(imap->debug)
+                                                        fprint(2, "<- ignoring %.*s\n",
+                                                                Blinelen(&imap->bin), p);
+                                        }else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
+                                                *r = '\0';
+                                                q++;
+                                                n = r-q;
+                                                if(imap->data == nil || n >= imap->size)
+                                                        imapgrow(imap, n);
+                                                memmove(imap->data, q, n);
+                                                imap->data[n] = '\0';
+                                                imap->data += n;
+                                                imap->size -= n;
+                                        }else
+                                                return "confused about FETCH response";
+                                        break;
+                                }
+
+                                // * 1 FETCH (UID 1 RFC822.SIZE 511)
+                                if(q=strstr(p, "RFC822.SIZE")){
+                                        imap->size = atoi(q+11);
+                                        break;
+                                }
+
+                                // * 1 FETCH (UID 1 RFC822.HEADER {496}
+                                // <496 bytes of data>
+                                 // )
+                                // * 1 FETCH (UID 1 RFC822.HEADER "data")
+                                if(strstr(p, "RFC822.HEADER") || strstr(p, "RFC822.TEXT")){
+                                        if((q = strchr(p, '{')) 
+                                        && (n=strtol(q+1, &en, 0), *en=='}')){
+                                                if(imap->data == nil || n >= imap->size)
+                                                        imapgrow(imap, n);
+                                                if((i = Bread(&imap->bin, imap->data, n)) != n){
+                                                        snprint(error, sizeof error,
+                                                                "short read %d != %d: %r\n",
+                                                                i, n);
+                                                        return error;
+                                                }
+                                                if(imap->debug)
+                                                        fprint(2, "<- read %d bytes\n", n);
+                                                imap->data[n] = '\0';
+                                                if(imap->debug)
+                                                        fprint(2, "<- %s\n", imap->data);
+                                                imap->data += n;
+                                                imap->size -= n;
+                                                p = Brdline(&imap->bin, '\n');
+                                                if(imap->debug)
+                                                        fprint(2, "<- ignoring %.*s\n",
+                                                                Blinelen(&imap->bin), p);
+                                        }else if((q = strchr(p, '"')) && (r = strchr(q+1, '"'))){
+                                                *r = '\0';
+                                                q++;
+                                                n = r-q;
+                                                if(imap->data == nil || n >= imap->size)
+                                                        imapgrow(imap, n);
+                                                memmove(imap->data, q, n);
+                                                imap->data[n] = '\0';
+                                                imap->data += n;
+                                                imap->size -= n;
+                                        }else
+                                                return "confused about FETCH response";
+                                        break;
+                                }
+
+                                // * 1 FETCH (UID 1)
+                                // * 2 FETCH (UID 6)
+                                if(q = strstr(p, "UID")){
+                                        if(imap->nuid < imap->muid)
+                                                imap->uid[imap->nuid++] = ((vlong)imap->validity<<32)|strtoul(q+3, nil, 10);
+                                        break;
+                                }
+                        }
+
+                        if(imap->tag == 0)
+                                return line;
+                        break;
+
+                case '9':                // response to our message
+                        op = p;
+                        if(p[1]=='X' && strtoul(p+2, &p, 10)==imap->tag){
+                                while(*p==' ')
+                                        p++;
+                                imap->tag++;
+                                return p;
+                        }
+                        fprint(2, "expected %lud; got %s\n", imap->tag, op);
+                        break;
+
+                default:
+                        if(imap->debug || *p)
+                                fprint(2, "unexpected line: %s\n", p);
+                }
+        }
+        snprint(error, sizeof error, "i/o error: %r\n");
+        return error;
+}
+
+static int
+isokay(char *resp)
+{
+        return strncmp(resp, "OK", 2)==0;
+}
+
+//
+// log in to IMAP4 server, select mailbox, no SSL at the moment
+//
+static char*
+imap4login(Imap *imap)
+{
+        char *s;
+        UserPasswd *up;
+
+        imap->tag = 0;
+        s = imap4resp(imap);
+        if(!isokay(s))
+                return "error in initial IMAP handshake";
+
+        if(imap->user != nil)
+                up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q user=%q", imap->host, imap->user);
+        else
+                up = auth_getuserpasswd(auth_getkey, "proto=pass service=imap server=%q", imap->host);
+        if(up == nil)
+                return "cannot find IMAP password";
+
+        imap->tag = 1;
+        imap4cmd(imap, "LOGIN %Z %Z", up->user, up->passwd);
+        free(up);
+        if(!isokay(s = imap4resp(imap)))
+                return s;
+
+        imap4cmd(imap, "SELECT %Z", imap->mbox);
+        if(!isokay(s = imap4resp(imap)))
+                return s;
+
+        return nil;
+}
+
+//
+// push tls onto a connection
+//
+int
+mypushtls(int fd)
+{
+        int p[2];
+        char buf[10];
+
+        if(pipe(p) < 0)
+                return -1;
+
+        switch(fork()){
+        case -1:
+                close(p[0]);
+                close(p[1]);
+                return -1;
+        case 0:
+                close(p[1]);
+                dup(p[0], 0);
+                dup(p[0], 1);
+                sprint(buf, "/fd/%d", fd);
+                execl("/bin/tlsrelay", "tlsrelay", "-f", buf, nil);
+                _exits(nil);
+        default:
+                break;
+        }
+        close(fd);
+        close(p[0]);
+        return p[1];
+}
+
+//
+// dial and handshake with the imap server
+//
+static char*
+imap4dial(Imap *imap)
+{
+        char *err, *port;
+        uchar digest[SHA1dlen];
+        int sfd;
+        TLSconn conn;
+
+        if(imap->fd >= 0){
+                imap4cmd(imap, "noop");
+                if(isokay(imap4resp(imap)))
+                        return nil;
+                close(imap->fd);
+                imap->fd = -1;
+        }
+
+        if(imap->mustssl)
+                port = "imaps";
+        else
+                port = "imap4";
+
+        if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
+                return geterrstr();
+
+        if(imap->mustssl){
+                memset(&conn, 0, sizeof conn);
+                sfd = tlsClient(imap->fd, &conn);
+                if(sfd < 0)
+                        sysfatal("tlsClient: %r");
+                if(conn.cert==nil || conn.certlen <= 0)
+                        sysfatal("server did not provide TLS certificate");
+                sha1(conn.cert, conn.certlen, digest, nil);
+                if(!imap->thumb || !okThumbprint(digest, imap->thumb)){
+                        fmtinstall('H', encodefmt);
+                        sysfatal("server certificate %.*H not recognized", SHA1dlen, digest);
+                }
+                free(conn.cert);
+                close(imap->fd);
+                imap->fd = sfd;
+
+                if(imap->debug){
+                        char fn[128];
+                        int fd;
+
+                        snprint(fn, sizeof fn, "%s/ctl", conn.dir);
+                        fd = open(fn, ORDWR);
+                        if(fd < 0)
+                                fprint(2, "opening ctl: %r\n");
+                        if(fprint(fd, "debug") < 0)
+                                fprint(2, "writing ctl: %r\n");
+                        close(fd);
+                }
+        }
+        Binit(&imap->bin, imap->fd, OREAD);
+        Binit(&imap->bout, imap->fd, OWRITE);
+
+        if(err = imap4login(imap)) {
+                close(imap->fd);
+                return err;
+        }
+
+        return nil;
+}
+
+//
+// close connection
+//
+#if 0  /* jpc */
+static void
+imap4hangup(Imap *imap)
+{
+        imap4cmd(imap, "LOGOUT");
+        imap4resp(imap);
+        close(imap->fd);
+}
+#endif
+
+//
+// download a single message
+//
+static char*
+imap4fetch(Mailbox *mb, Message *m)
+{
+        int i;
+        char *p, *s, sdigest[2*SHA1dlen+1];
+        Imap *imap;
+
+        imap = mb->aux;
+
+        imap->size = 0;
+
+        if(!isokay(s = imap4resp(imap)))
+                return s;
+
+        p = imap->base;
+        if(p == nil)
+                return "did not get message body";
+
+        removecr(p);
+        free(m->start);
+        m->start = p;
+        m->end = p+strlen(p);
+        m->bend = m->rbend = m->end;
+        m->header = m->start;
+
+        imap->base = nil;
+        imap->data = nil;
+
+        parse(m, 0, mb, 1);
+
+        // digest headers
+        sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
+        for(i = 0; i < SHA1dlen; i++)
+                sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
+        m->sdigest = s_copy(sdigest);
+
+        return nil;
+}
+
+//
+// check for new messages on imap4 server
+// download new messages, mark deleted messages
+//
+static char*
+imap4read(Imap *imap, Mailbox *mb, int doplumb)
+{
+        char *s;
+        int i, ignore, nnew, t;
+        Message *m, *next, **l;
+
+        imap4cmd(imap, "STATUS %Z (MESSAGES UIDVALIDITY)", imap->mbox);
+        if(!isokay(s = imap4resp(imap)))
+                return s;
+
+        imap->nuid = 0;
+        imap->uid = erealloc(imap->uid, imap->nmsg*sizeof(imap->uid[0]));
+        imap->muid = imap->nmsg;
+
+        if(imap->nmsg > 0){
+                imap4cmd(imap, "UID FETCH 1:* UID");
+                if(!isokay(s = imap4resp(imap)))
+                        return s;
+        }
+
+        l = &mb->root->part;
+        for(i=0; inuid; i++){
+                ignore = 0;
+                while(*l != nil){
+                        if((*l)->imapuid == imap->uid[i]){
+                                ignore = 1;
+                                l = &(*l)->next;
+                                break;
+                        }else{
+                                // old mail, we don't have it anymore
+                                if(doplumb)
+                                        mailplumb(mb, *l, 1);
+                                (*l)->inmbox = 0;
+                                (*l)->deleted = 1;
+                                l = &(*l)->next;
+                        }
+                }
+                if(ignore)
+                        continue;
+
+                // new message
+                m = newmessage(mb->root);
+                m->mallocd = 1;
+                m->inmbox = 1;
+                m->imapuid = imap->uid[i];
+
+                // add to chain, will download soon
+                *l = m;
+                l = &m->next;
+        }
+
+        // whatever is left at the end of the chain is gone
+        while(*l != nil){
+                if(doplumb)
+                        mailplumb(mb, *l, 1);
+                (*l)->inmbox = 0;
+                (*l)->deleted = 1;
+                l = &(*l)->next;
+        }
+
+        // download new messages
+        t = imap->tag;
+        if(pipeline)
+        switch(rfork(RFPROC|RFMEM)){
+        case -1:
+                sysfatal("rfork: %r");
+        default:
+                break;
+        case 0:
+                for(m = mb->root->part; m != nil; m = m->next){
+                        if(m->start != nil)
+                                continue;
+                        if(imap->debug)
+                                fprint(2, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
+                                        t, (ulong)m->imapuid);
+                        Bprint(&imap->bout, "9X%d UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
+                                t++, (ulong)m->imapuid);
+                }
+                Bflush(&imap->bout);
+                _exits(nil);
+        }
+
+        nnew = 0;
+        for(m=mb->root->part; m!=nil; m=next){
+                next = m->next;
+                if(m->start != nil)
+                        continue;
+
+                if(!pipeline){
+                        Bprint(&imap->bout, "9X%lud UID FETCH %lud (UID RFC822.SIZE BODY[])\r\n",
+                                (ulong)imap->tag, (ulong)m->imapuid);
+                        Bflush(&imap->bout);
+                }
+
+                if(s = imap4fetch(mb, m)){
+                        // message disappeared?  unchain
+                        fprint(2, "download %lud: %s\n", (ulong)m->imapuid, s);
+                        delmessage(mb, m);
+                        mb->root->subname--;
+                        continue;
+                }
+                nnew++;
+                if(doplumb)
+                        mailplumb(mb, m, 0);
+        }
+        if(pipeline)
+                waitpid();
+
+        if(nnew || mb->vers == 0){
+                mb->vers++;
+                henter(PATH(0, Qtop), mb->name,
+                        (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+        }
+        return nil;
+}
+
+//
+// sync mailbox
+//
+static void
+imap4purge(Imap *imap, Mailbox *mb)
+{
+        int ndel;
+        Message *m, *next;
+
+        ndel = 0;
+        for(m=mb->root->part; m!=nil; m=next){
+                next = m->next;
+                if(m->deleted && m->refs==0){
+                        if(m->inmbox && (ulong)(m->imapuid>>32)==imap->validity){
+                                imap4cmd(imap, "UID STORE %lud +FLAGS (\\Deleted)", (ulong)m->imapuid);
+                                if(isokay(imap4resp(imap))){
+                                        ndel++;
+                                        delmessage(mb, m);
+                                }
+                        }else
+                                delmessage(mb, m);
+                }
+        }
+
+        if(ndel){
+                imap4cmd(imap, "EXPUNGE");
+                imap4resp(imap);
+        }
+}
+
+//
+// connect to imap4 server, sync mailbox
+//
+static char*
+imap4sync(Mailbox *mb, int doplumb)
+{
+        char *err;
+        Imap *imap;
+
+        imap = mb->aux;
+
+        if(err = imap4dial(imap)){
+                mb->waketime = time(0) + imap->refreshtime;
+                return err;
+        }
+
+        if((err = imap4read(imap, mb, doplumb)) == nil){
+                imap4purge(imap, mb);
+                mb->d->atime = mb->d->mtime = time(0);
+        }
+        /*
+         * don't hang up; leave connection open for next time.
+         */
+        // imap4hangup(imap);
+        mb->waketime = time(0) + imap->refreshtime;
+        return err;
+}
+
+static char Eimap4ctl[] = "bad imap4 control message";
+
+static char*
+imap4ctl(Mailbox *mb, int argc, char **argv)
+{
+        int n;
+        Imap *imap;
+
+        imap = mb->aux;
+        if(argc < 1)
+                return Eimap4ctl;
+
+        if(argc==1 && strcmp(argv[0], "debug")==0){
+                imap->debug = 1;
+                return nil;
+        }
+
+        if(argc==1 && strcmp(argv[0], "nodebug")==0){
+                imap->debug = 0;
+                return nil;
+        }
+
+        if(argc==1 && strcmp(argv[0], "thumbprint")==0){
+                if(imap->thumb)
+                        freeThumbprints(imap->thumb);
+                imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
+        }
+        if(strcmp(argv[0], "refresh")==0){
+                if(argc==1){
+                        imap->refreshtime = 60;
+                        return nil;
+                }
+                if(argc==2){
+                        n = atoi(argv[1]);
+                        if(n < 15)
+                                return Eimap4ctl;
+                        imap->refreshtime = n;
+                        return nil;
+                }
+        }
+
+        return Eimap4ctl;
+}
+
+//
+// free extra memory associated with mb
+//
+static void
+imap4close(Mailbox *mb)
+{
+        Imap *imap;
+
+        imap = mb->aux;
+        free(imap->freep);
+        free(imap->base);
+        free(imap->uid);
+        if(imap->fd >= 0)
+                close(imap->fd);
+        free(imap);
+}
+
+//
+// open mailboxes of the form /imap/host/user
+//
+char*
+imap4mbox(Mailbox *mb, char *path)
+{
+        char *f[10];
+        int mustssl, nf;
+        Imap *imap;
+
+        quotefmtinstall();
+        fmtinstall('Z', doublequote);
+        if(strncmp(path, "/imap/", 6) != 0 && strncmp(path, "/imaps/", 7) != 0)
+                return Enotme;
+        mustssl = (strncmp(path, "/imaps/", 7) == 0);
+
+        path = strdup(path);
+        if(path == nil)
+                return "out of memory";
+
+        nf = getfields(path, f, 5, 0, "/");
+        if(nf < 3){
+                free(path);
+                return "bad imap path syntax /imap[s]/system[/user[/mailbox]]";
+        }
+
+        imap = emalloc(sizeof(*imap));
+        imap->fd = -1;
+        imap->debug = debug;
+        imap->freep = path;
+        imap->mustssl = mustssl;
+        imap->host = f[2];
+        if(nf < 4)
+                imap->user = nil;
+        else
+                imap->user = f[3];
+        if(nf < 5)
+                imap->mbox = "Inbox";
+        else
+                imap->mbox = f[4];
+        imap->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude");
+
+        mb->aux = imap;
+        mb->sync = imap4sync;
+        mb->close = imap4close;
+        mb->ctl = imap4ctl;
+        mb->d = emalloc(sizeof(*mb->d));
+        //mb->fetch = imap4fetch;
+
+        return nil;
+}
+
+//
+// Formatter for %"
+// Use double quotes to protect white space, frogs, \ and "
+//
+enum
+{
+        Qok = 0,
+        Qquote,
+        Qbackslash,
+};
+
+static int
+needtoquote(Rune r)
+{
+        if(r >= Runeself)
+                return Qquote;
+        if(r <= ' ')
+                return Qquote;
+        if(r=='\\' || r=='"')
+                return Qbackslash;
+        return Qok;
+}
+
+int
+doublequote(Fmt *f)
+{
+        char *s, *t;
+        int w, quotes;
+        Rune r;
+
+        s = va_arg(f->args, char*);
+        if(s == nil || *s == '\0')
+                return fmtstrcpy(f, "\"\"");
+
+        quotes = 0;
+        for(t=s; *t; t+=w){
+                w = chartorune(&r, t);
+                quotes |= needtoquote(r);
+        }
+        if(quotes == 0)
+                return fmtstrcpy(f, s);
+
+        fmtrune(f, '"');
+        for(t=s; *t; t+=w){
+                w = chartorune(&r, t);
+                if(needtoquote(r) == Qbackslash)
+                        fmtrune(f, '\\');
+                fmtrune(f, r);
+        }
+        return fmtrune(f, '"');
+}
diff --git a/src/cmd/upas/fs/mbox.c b/src/cmd/upas/fs/mbox.c
t@@ -0,0 +1,1601 @@
+#include "common.h"
+#include 
+#include 
+#include 
+#include 
+#include "dat.h"
+
+extern char* dirtab[]; /* jpc */
+
+typedef struct Header Header;
+
+struct Header {
+        char *type;
+        void (*f)(Message*, Header*, char*);
+        int len;
+};
+
+/* headers */
+static        void        ctype(Message*, Header*, char*);
+static        void        cencoding(Message*, Header*, char*);
+static        void        cdisposition(Message*, Header*, char*);
+static        void        date822(Message*, Header*, char*);
+static        void        from822(Message*, Header*, char*);
+static        void        to822(Message*, Header*, char*);
+static        void        sender822(Message*, Header*, char*);
+static        void        replyto822(Message*, Header*, char*);
+static        void        subject822(Message*, Header*, char*);
+static        void        inreplyto822(Message*, Header*, char*);
+static        void        cc822(Message*, Header*, char*);
+static        void        bcc822(Message*, Header*, char*);
+static        void        messageid822(Message*, Header*, char*);
+static        void        mimeversion(Message*, Header*, char*);
+static        void        nullsqueeze(Message*);
+enum
+{
+        Mhead=        11,        /* offset of first mime header */
+};
+
+Header head[] =
+{
+        { "date:", date822, },
+        { "from:", from822, },
+        { "to:", to822, },
+        { "sender:", sender822, },
+        { "reply-to:", replyto822, },
+        { "subject:", subject822, },
+        { "cc:", cc822, },
+        { "bcc:", bcc822, },
+        { "in-reply-to:", inreplyto822, },
+        { "mime-version:", mimeversion, },
+        { "message-id:", messageid822, },
+
+[Mhead]        { "content-type:", ctype, },
+        { "content-transfer-encoding:", cencoding, },
+        { "content-disposition:", cdisposition, },
+        { 0, },
+};
+
+/* static        void        fatal(char *fmt, ...); jpc */
+static        void        initquoted(void);
+/* static        void        startheader(Message*);
+static        void        startbody(Message*); jpc */
+static        char*        skipwhite(char*);
+static        char*        skiptosemi(char*);
+static        char*        getstring(char*, String*, int);
+static        void        setfilename(Message*, char*);
+/* static        char*        lowercase(char*); jpc */
+static        int        is8bit(Message*);
+static        int        headerline(char**, String*);
+static        void        initheaders(void);
+static void        parseattachments(Message*, Mailbox*);
+
+int                debug;
+
+char *Enotme = "path not served by this file server";
+
+enum
+{
+        Chunksize = 1024,
+};
+
+Mailboxinit *boxinit[] = {
+        imap4mbox,
+        pop3mbox,
+        plan9mbox,
+};
+
+char*
+syncmbox(Mailbox *mb, int doplumb)
+{
+        return (*mb->sync)(mb, doplumb);
+}
+
+/* create a new mailbox */
+char*
+newmbox(char *path, char *name, int std)
+{
+        Mailbox *mb, **l;
+        char *p, *rv;
+        int i;
+
+        initheaders();
+
+        mb = emalloc(sizeof(*mb));
+        strncpy(mb->path, path, sizeof(mb->path)-1);
+        if(name == nil){
+                p = strrchr(path, '/');
+                if(p == nil)
+                        p = path;
+                else
+                        p++;
+                if(*p == 0){
+                        free(mb);
+                        return "bad mbox name";
+                }
+                strncpy(mb->name, p, sizeof(mb->name)-1);
+        } else {
+                strncpy(mb->name, name, sizeof(mb->name)-1);
+        }
+
+        rv = nil;
+        // check for a mailbox type
+        for(i=0; inext){
+                if(strcmp((*l)->name, mb->name) == 0){
+                        if(strcmp(path, (*l)->path) == 0)
+                                rv = nil;
+                        else
+                                rv = "mbox name in use";
+                        if(mb->close)
+                                (*mb->close)(mb);
+                        free(mb);
+                        qunlock(&mbllock);
+                        return rv;
+                }
+        }
+
+        // always try locking
+        mb->dolock = 1;
+
+        mb->refs = 1;
+        mb->next = nil;
+        mb->id = newid();
+        mb->root = newmessage(nil);
+        mb->std = std;
+        *l = mb;
+        qunlock(&mbllock);
+
+        qlock(&mb->ql);
+        if(mb->ctl){
+                henter(PATH(mb->id, Qmbox), "ctl",
+                        (Qid){PATH(mb->id, Qmboxctl), 0, QTFILE}, nil, mb);
+        }
+        rv = syncmbox(mb, 0);
+        qunlock(&mb->ql);
+
+        return rv;
+}
+
+// close the named mailbox
+void
+freembox(char *name)
+{
+        Mailbox **l, *mb;
+
+        qlock(&mbllock);
+        for(l=&mbl; *l != nil; l=&(*l)->next){
+                if(strcmp(name, (*l)->name) == 0){
+                        mb = *l;
+                        *l = mb->next;
+                        mboxdecref(mb);
+                        break;
+                }
+        }
+        hfree(PATH(0, Qtop), name);
+        qunlock(&mbllock);
+}
+
+static void
+initheaders(void)
+{
+        Header *h;
+        static int already;
+
+        if(already)
+                return;
+        already = 1;
+
+        for(h = head; h->type != nil; h++)
+                h->len = strlen(h->type);
+}
+
+/*
+ *  parse a Unix style header
+ */
+void
+parseunix(Message *m)
+{
+        char *p;
+        String *h;
+
+        h = s_new();
+        for(p = m->start + 5; *p && *p != '\r' && *p != '\n'; p++)
+                s_putc(h, *p);
+        s_terminate(h);
+        s_restart(h);
+
+        m->unixfrom = s_parse(h, s_reset(m->unixfrom));
+        m->unixdate = s_append(s_reset(m->unixdate), h->ptr);
+
+        s_free(h);
+}
+
+/*
+ *  parse a message
+ */
+void
+parseheaders(Message *m, int justmime, Mailbox *mb, int addfrom)
+{
+        String *hl;
+        Header *h;
+        char *p, *q;
+        int i;
+
+        if(m->whole == m->whole->whole){
+                henter(PATH(mb->id, Qmbox), m->name,
+                        (Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
+        } else {
+                henter(PATH(m->whole->id, Qdir), m->name,
+                        (Qid){PATH(m->id, Qdir), 0, QTDIR}, m, mb);
+        }
+        for(i = 0; i < Qmax; i++)
+                henter(PATH(m->id, Qdir), dirtab[i],
+                        (Qid){PATH(m->id, i), 0, QTFILE}, m, mb);
+
+        // parse mime headers
+        p = m->header;
+        hl = s_new();
+        while(headerline(&p, hl)){
+                if(justmime)
+                        h = &head[Mhead];
+                else
+                        h = head;
+                for(; h->type; h++){
+                        if(cistrncmp(s_to_c(hl), h->type, h->len) == 0){
+                                (*h->f)(m, h, s_to_c(hl));
+                                break;
+                        }
+                }
+                s_reset(hl);
+        }
+        s_free(hl);
+
+        // the blank line isn't really part of the body or header
+        if(justmime){
+                m->mhend = p;
+                m->hend = m->header;
+        } else {
+                m->hend = p;
+        }
+        if(*p == '\n')
+                p++;
+        m->rbody = m->body = p;
+
+        // if type is text, get any nulls out of the body.  This is
+        // for the two seans and imap clients that get confused.
+        if(strncmp(s_to_c(m->type), "text/", 5) == 0)
+                nullsqueeze(m);
+
+        //
+        // cobble together Unix-style from line
+        // for local mailbox messages, we end up recreating the
+        // original header.
+        // for pop3 messages, the best we can do is 
+        // use the From: information and the RFC822 date.
+        //
+        if(m->unixdate == nil || strcmp(s_to_c(m->unixdate), "???") == 0
+        || strcmp(s_to_c(m->unixdate), "Thu Jan 1 00:00:00 GMT 1970") == 0){
+                if(m->unixdate){
+                        s_free(m->unixdate);
+                        m->unixdate = nil;
+                }
+                // look for the date in the first Received: line.
+                // it's likely to be the right time zone (it's
+                 // the local system) and in a convenient format.
+                if(cistrncmp(m->header, "received:", 9)==0){
+                        if((q = strchr(m->header, ';')) != nil){
+                                p = q;
+                                while((p = strchr(p, '\n')) != nil){
+                                        if(p[1] != ' ' && p[1] != '\t' && p[1] != '\n')
+                                                break;
+                                        p++;
+                                }
+                                if(p){
+                                        *p = '\0';
+                                        m->unixdate = date822tounix(q+1);
+                                        *p = '\n';
+                                }
+                        }
+                }
+
+                // fall back on the rfc822 date        
+                if(m->unixdate==nil && m->date822)
+                        m->unixdate = date822tounix(s_to_c(m->date822));
+        }
+
+        if(m->unixheader != nil)
+                s_free(m->unixheader);
+
+        // only fake header for top-level messages for pop3 and imap4
+        // clients (those protocols don't include the unix header).
+        // adding the unix header all the time screws up mime-attached
+        // rfc822 messages.
+        if(!addfrom && !m->unixfrom){
+                m->unixheader = nil;
+                return;
+        }
+
+        m->unixheader = s_copy("From ");
+        if(m->unixfrom && strcmp(s_to_c(m->unixfrom), "???") != 0)
+                s_append(m->unixheader, s_to_c(m->unixfrom));
+        else if(m->from822)
+                s_append(m->unixheader, s_to_c(m->from822));
+        else
+                s_append(m->unixheader, "???");
+
+        s_append(m->unixheader, " ");
+        if(m->unixdate)
+                s_append(m->unixheader, s_to_c(m->unixdate));
+        else
+                s_append(m->unixheader, "Thu Jan  1 00:00:00 GMT 1970");
+
+        s_append(m->unixheader, "\n");
+}
+
+String*
+promote(String **sp)
+{
+        String *s;
+
+        if(*sp != nil)
+                s = s_clone(*sp);
+        else
+                s = nil;
+        return s;
+}
+
+void
+parsebody(Message *m, Mailbox *mb)
+{
+        Message *nm;
+
+        // recurse
+        if(strncmp(s_to_c(m->type), "multipart/", 10) == 0){
+                parseattachments(m, mb);
+        } else if(strcmp(s_to_c(m->type), "message/rfc822") == 0){
+                decode(m);
+                parseattachments(m, mb);
+                nm = m->part;
+
+                // promote headers
+                if(m->replyto822 == nil && m->from822 == nil && m->sender822 == nil){
+                        m->from822 = promote(&nm->from822);
+                        m->to822 = promote(&nm->to822);
+                        m->date822 = promote(&nm->date822);
+                        m->sender822 = promote(&nm->sender822);
+                        m->replyto822 = promote(&nm->replyto822);
+                        m->subject822 = promote(&nm->subject822);
+                        m->unixdate = promote(&nm->unixdate);
+                }
+        }
+}
+
+void
+parse(Message *m, int justmime, Mailbox *mb, int addfrom)
+{
+        parseheaders(m, justmime, mb, addfrom);
+        parsebody(m, mb);
+}
+
+static void
+parseattachments(Message *m, Mailbox *mb)
+{
+        Message *nm, **l;
+        char *p, *x;
+
+        // if there's a boundary, recurse...
+        if(m->boundary != nil){
+                p = m->body;
+                nm = nil;
+                l = &m->part;
+                for(;;){
+                        x = strstr(p, s_to_c(m->boundary));
+
+                        /* no boundary, we're done */
+                        if(x == nil){
+                                if(nm != nil)
+                                        nm->rbend = nm->bend = nm->end = m->bend;
+                                break;
+                        }
+
+                        /* boundary must be at the start of a line */
+                        if(x != m->body && *(x-1) != '\n'){
+                                p = x+1;
+                                continue;
+                        }
+
+                        if(nm != nil)
+                                nm->rbend = nm->bend = nm->end = x;
+                        x += strlen(s_to_c(m->boundary));
+
+                        /* is this the last part? ignore anything after it */
+                        if(strncmp(x, "--", 2) == 0)
+                                break;
+
+                        p = strchr(x, '\n');
+                        if(p == nil)
+                                break;
+                        nm = newmessage(m);
+                        nm->start = nm->header = nm->body = nm->rbody = ++p;
+                        nm->mheader = nm->header;
+                        *l = nm;
+                        l = &nm->next;
+                }
+                for(nm = m->part; nm != nil; nm = nm->next)
+                        parse(nm, 1, mb, 0);
+                return;
+        }
+
+        // if we've got an rfc822 message, recurse...
+        if(strcmp(s_to_c(m->type), "message/rfc822") == 0){
+                nm = newmessage(m);
+                m->part = nm;
+                nm->start = nm->header = nm->body = nm->rbody = m->body;
+                nm->end = nm->bend = nm->rbend = m->bend;
+                parse(nm, 0, mb, 0);
+        }
+}
+
+/*
+ *  pick up a header line
+ */
+static int
+headerline(char **pp, String *hl)
+{
+        char *p, *x;
+
+        s_reset(hl);
+        p = *pp;
+        x = strpbrk(p, ":\n");
+        if(x == nil || *x == '\n')
+                return 0;
+        for(;;){
+                x = strchr(p, '\n');
+                if(x == nil)
+                        x = p + strlen(p);
+                s_nappend(hl, p, x-p);
+                p = x;
+                if(*p != '\n' || *++p != ' ' && *p != '\t')
+                        break;
+                while(*p == ' ' || *p == '\t')
+                        p++;
+                s_putc(hl, ' ');
+        }
+        *pp = p;
+        return 1;
+}
+
+static String*
+addr822(char *p)
+{
+        String *s, *list;
+        int incomment, addrdone, inanticomment, quoted;
+        int n;
+        int c;
+
+        list = s_new();
+        s = s_new();
+        quoted = incomment = addrdone = inanticomment = 0;
+        n = 0;
+        for(; *p; p++){
+                c = *p;
+
+                // whitespace is ignored
+                if(!quoted && isspace(c) || c == '\r')
+                        continue;
+
+                // strings are always treated as atoms
+                if(!quoted && c == '"'){
+                        if(!addrdone && !incomment)
+                                s_putc(s, c);
+                        for(p++; *p; p++){
+                                if(!addrdone && !incomment)
+                                        s_putc(s, *p);
+                                if(!quoted && *p == '"')
+                                        break;
+                                if(*p == '\\')
+                                        quoted = 1;
+                                else
+                                        quoted = 0;
+                        }
+                        if(*p == 0)
+                                break;
+                        quoted = 0;
+                        continue;
+                }
+
+                // ignore everything in an expicit comment
+                if(!quoted && c == '('){
+                        incomment = 1;
+                        continue;
+                }
+                if(incomment){
+                        if(!quoted && c == ')')
+                                incomment = 0;
+                        quoted = 0;
+                        continue;
+                }
+
+                // anticomments makes everything outside of them comments
+                if(!quoted && c == '<' && !inanticomment){
+                        inanticomment = 1;
+                        s = s_reset(s);
+                        continue;
+                }
+                if(!quoted && c == '>' && inanticomment){
+                        addrdone = 1;
+                        inanticomment = 0;
+                        continue;
+                }
+
+                // commas separate addresses
+                if(!quoted && c == ',' && !inanticomment){
+                        s_terminate(s);
+                        addrdone = 0;
+                        if(n++ != 0)
+                                s_append(list, " ");
+                        s_append(list, s_to_c(s));
+                        s = s_reset(s);
+                        continue;
+                }
+
+                // what's left is part of the address
+                s_putc(s, c);
+
+                // quoted characters are recognized only as characters
+                if(c == '\\')
+                        quoted = 1;
+                else
+                        quoted = 0;
+
+        }
+
+        if(*s_to_c(s) != 0){
+                s_terminate(s);
+                if(n++ != 0)
+                        s_append(list, " ");
+                s_append(list, s_to_c(s));
+        }
+        s_free(s);
+
+        if(n == 0){
+                s_free(list);
+                return nil;
+        }
+        return list;
+}
+
+static void
+to822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        s_free(m->to822);
+        m->to822 = addr822(p);
+}
+
+static void
+cc822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        s_free(m->cc822);
+        m->cc822 = addr822(p);
+}
+
+static void
+bcc822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        s_free(m->bcc822);
+        m->bcc822 = addr822(p);
+}
+
+static void
+from822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        s_free(m->from822);
+        m->from822 = addr822(p);
+}
+
+static void
+sender822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        s_free(m->sender822);
+        m->sender822 = addr822(p);
+}
+
+static void
+replyto822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        s_free(m->replyto822);
+        m->replyto822 = addr822(p);
+}
+
+static void
+mimeversion(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        s_free(m->mimeversion);
+        m->mimeversion = addr822(p);
+}
+
+static void
+killtrailingwhite(char *p)
+{
+        char *e;
+
+        e = p + strlen(p) - 1;
+        while(e > p && isspace(*e))
+                *e-- = 0;
+}
+
+static void
+date822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        p = skipwhite(p);
+        s_free(m->date822);
+        m->date822 = s_copy(p);
+        p = s_to_c(m->date822);
+        killtrailingwhite(p);
+}
+
+static void
+subject822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        p = skipwhite(p);
+        s_free(m->subject822);
+        m->subject822 = s_copy(p);
+        p = s_to_c(m->subject822);
+        killtrailingwhite(p);
+}
+
+static void
+inreplyto822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        p = skipwhite(p);
+        s_free(m->inreplyto822);
+        m->inreplyto822 = s_copy(p);
+        p = s_to_c(m->inreplyto822);
+        killtrailingwhite(p);
+}
+
+static void
+messageid822(Message *m, Header *h, char *p)
+{
+        p += strlen(h->type);
+        p = skipwhite(p);
+        s_free(m->messageid822);
+        m->messageid822 = s_copy(p);
+        p = s_to_c(m->messageid822);
+        killtrailingwhite(p);
+}
+
+static int
+isattribute(char **pp, char *attr)
+{
+        char *p;
+        int n;
+
+        n = strlen(attr);
+        p = *pp;
+        if(cistrncmp(p, attr, n) != 0)
+                return 0;
+        p += n;
+        while(*p == ' ')
+                p++;
+        if(*p++ != '=')
+                return 0;
+        while(*p == ' ')
+                p++;
+        *pp = p;
+        return 1;
+}
+
+static void
+ctype(Message *m, Header *h, char *p)
+{
+        String *s;
+
+        p += h->len;
+        p = skipwhite(p);
+
+        p = getstring(p, m->type, 1);
+        
+        while(*p){
+                if(isattribute(&p, "boundary")){
+                        s = s_new();
+                        p = getstring(p, s, 0);
+                        m->boundary = s_reset(m->boundary);
+                        s_append(m->boundary, "--");
+                        s_append(m->boundary, s_to_c(s));
+                        s_free(s);
+                } else if(cistrncmp(p, "multipart", 9) == 0){
+                        /*
+                         *  the first unbounded part of a multipart message,
+                         *  the preamble, is not displayed or saved
+                         */
+                } else if(isattribute(&p, "name")){
+                        if(m->filename == nil)
+                                setfilename(m, p);
+                } else if(isattribute(&p, "charset")){
+                        p = getstring(p, s_reset(m->charset), 0);
+                }
+                
+                p = skiptosemi(p);
+        }
+}
+
+static void
+cencoding(Message *m, Header *h, char *p)
+{
+        p += h->len;
+        p = skipwhite(p);
+        if(cistrncmp(p, "base64", 6) == 0)
+                m->encoding = Ebase64;
+        else if(cistrncmp(p, "quoted-printable", 16) == 0)
+                m->encoding = Equoted;
+}
+
+static void
+cdisposition(Message *m, Header *h, char *p)
+{
+        p += h->len;
+        p = skipwhite(p);
+        while(*p){
+                if(cistrncmp(p, "inline", 6) == 0){
+                        m->disposition = Dinline;
+                } else if(cistrncmp(p, "attachment", 10) == 0){
+                        m->disposition = Dfile;
+                } else if(cistrncmp(p, "filename=", 9) == 0){
+                        p += 9;
+                        setfilename(m, p);
+                }
+                p = skiptosemi(p);
+        }
+
+}
+
+ulong msgallocd, msgfreed;
+
+Message*
+newmessage(Message *parent)
+{
+        /* static int id; jpc */
+        Message *m;
+
+        msgallocd++;
+
+        m = emalloc(sizeof(*m));
+        memset(m, 0, sizeof(*m));
+        m->disposition = Dnone;
+        m->type = s_copy("text/plain");
+        m->charset = s_copy("iso-8859-1");
+        m->id = newid();
+        if(parent)
+                sprint(m->name, "%d", ++(parent->subname));
+        if(parent == nil)
+                parent = m;
+        m->whole = parent;
+        m->hlen = -1;
+        return m;
+}
+
+// delete a message from a mailbox
+void
+delmessage(Mailbox *mb, Message *m)
+{
+        Message **l;
+        int i;
+
+        mb->vers++;
+        msgfreed++;
+
+        if(m->whole != m){
+                // unchain from parent
+                for(l = &m->whole->part; *l && *l != m; l = &(*l)->next)
+                        ;
+                if(*l != nil)
+                        *l = m->next;
+
+                // clear out of name lookup hash table
+                if(m->whole->whole == m->whole)
+                        hfree(PATH(mb->id, Qmbox), m->name);
+                else
+                        hfree(PATH(m->whole->id, Qdir), m->name);
+                for(i = 0; i < Qmax; i++)
+                        hfree(PATH(m->id, Qdir), dirtab[i]);
+        }
+
+        /* recurse through sub-parts */
+        while(m->part)
+                delmessage(mb, m->part);
+
+        /* free memory */
+        if(m->mallocd)
+                free(m->start);
+        if(m->hallocd)
+                free(m->header);
+        if(m->ballocd)
+                free(m->body);
+        s_free(m->unixfrom);
+        s_free(m->unixdate);
+        s_free(m->unixheader);
+        s_free(m->from822);
+        s_free(m->sender822);
+        s_free(m->to822);
+        s_free(m->bcc822);
+        s_free(m->cc822);
+        s_free(m->replyto822);
+        s_free(m->date822);
+        s_free(m->inreplyto822);
+        s_free(m->subject822);
+        s_free(m->messageid822);
+        s_free(m->addrs);
+        s_free(m->mimeversion);
+        s_free(m->sdigest);
+        s_free(m->boundary);
+        s_free(m->type);
+        s_free(m->charset);
+        s_free(m->filename);
+
+        free(m);
+}
+
+// mark messages (identified by path) for deletion
+void
+delmessages(int ac, char **av)
+{
+        Mailbox *mb;
+        Message *m;
+        int i, needwrite;
+
+        qlock(&mbllock);
+        for(mb = mbl; mb != nil; mb = mb->next)
+                if(strcmp(av[0], mb->name) == 0){
+                        qlock(&mb->ql);
+                        break;
+                }
+        qunlock(&mbllock);
+        if(mb == nil)
+                return;
+
+        needwrite = 0;
+        for(i = 1; i < ac; i++){
+                for(m = mb->root->part; m != nil; m = m->next)
+                        if(strcmp(m->name, av[i]) == 0){
+                                if(!m->deleted){
+                                        mailplumb(mb, m, 1);
+                                        needwrite = 1;
+                                        m->deleted = 1;
+                                        logmsg("deleting", m);
+                                }
+                                break;
+                        }
+        }
+        if(needwrite)
+                syncmbox(mb, 1);
+        qunlock(&mb->ql);
+}
+
+/*
+ *  the following are called with the mailbox qlocked
+ */
+void
+msgincref(Message *m)
+{
+        m->refs++;
+}
+void
+msgdecref(Mailbox *mb, Message *m)
+{
+        m->refs--;
+        if(m->refs == 0 && m->deleted)
+                syncmbox(mb, 1);
+}
+
+/*
+ *  the following are called with mbllock'd
+ */
+void
+mboxincref(Mailbox *mb)
+{
+        assert(mb->refs > 0);
+        mb->refs++;
+}
+void
+mboxdecref(Mailbox *mb)
+{
+        assert(mb->refs > 0);
+        qlock(&mb->ql);
+        mb->refs--;
+        if(mb->refs == 0){
+                delmessage(mb, mb->root);
+                if(mb->ctl)
+                        hfree(PATH(mb->id, Qmbox), "ctl");
+                if(mb->close)
+                        (*mb->close)(mb);
+                free(mb);
+        } else
+                qunlock(&mb->ql);
+}
+
+int
+cistrncmp(char *a, char *b, int n)
+{
+        while(n-- > 0){
+                if(tolower(*a++) != tolower(*b++))
+                        return -1;
+        }
+        return 0;
+}
+
+int
+cistrcmp(char *a, char *b)
+{
+        for(;;){
+                if(tolower(*a) != tolower(*b++))
+                        return -1;
+                if(*a++ == 0)
+                        break;
+        }
+        return 0;
+}
+
+static char*
+skipwhite(char *p)
+{
+        while(isspace(*p))
+                p++;
+        return p;
+}
+
+static char*
+skiptosemi(char *p)
+{
+        while(*p && *p != ';')
+                p++;
+        while(*p == ';' || isspace(*p))
+                p++;
+        return p;
+}
+
+static char*
+getstring(char *p, String *s, int dolower)
+{
+        s = s_reset(s);
+        p = skipwhite(p);
+        if(*p == '"'){
+                p++;
+                for(;*p && *p != '"'; p++)
+                        if(dolower)
+                                s_putc(s, tolower(*p));
+                        else
+                                s_putc(s, *p);
+                if(*p == '"')
+                        p++;
+                s_terminate(s);
+
+                return p;
+        }
+
+        for(; *p && !isspace(*p) && *p != ';'; p++)
+                if(dolower)
+                        s_putc(s, tolower(*p));
+                else
+                        s_putc(s, *p);
+        s_terminate(s);
+
+        return p;
+}
+
+static void
+setfilename(Message *m, char *p)
+{
+        m->filename = s_reset(m->filename);
+        getstring(p, m->filename, 0);
+        for(p = s_to_c(m->filename); *p; p++)
+                if(*p == ' ' || *p == '\t' || *p == ';')
+                        *p = '_';
+}
+
+//
+// undecode message body
+//
+void
+decode(Message *m)
+{
+        int i, len;
+        char *x;
+
+        if(m->decoded)
+                return;
+        switch(m->encoding){
+        case Ebase64:
+                len = m->bend - m->body;
+                i = (len*3)/4+1;        // room for max chars + null
+                x = emalloc(i);
+                len = dec64((uchar*)x, i, m->body, len);
+                if(m->ballocd)
+                        free(m->body);
+                m->body = x;
+                m->bend = x + len;
+                m->ballocd = 1;
+                break;
+        case Equoted:
+                len = m->bend - m->body;
+                x = emalloc(len+2);        // room for null and possible extra nl
+                len = decquoted(x, m->body, m->bend);
+                if(m->ballocd)
+                        free(m->body);
+                m->body = x;
+                m->bend = x + len;
+                m->ballocd = 1;
+                break;
+        default:
+                break;
+        }
+        m->decoded = 1;
+}
+
+// convert latin1 to utf
+void
+convert(Message *m)
+{
+        int len;
+        char *x;
+
+        // don't convert if we're not a leaf, not text, or already converted
+        if(m->converted)
+                return;
+        if(m->part != nil)
+                return;
+        if(cistrncmp(s_to_c(m->type), "text", 4) != 0)
+                return;
+
+        if(cistrcmp(s_to_c(m->charset), "us-ascii") == 0 ||
+           cistrcmp(s_to_c(m->charset), "iso-8859-1") == 0){
+                len = is8bit(m);
+                if(len > 0){
+                        len = 2*len + m->bend - m->body + 1;
+                        x = emalloc(len);
+                        len = latin1toutf(x, m->body, m->bend);
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        } else if(cistrcmp(s_to_c(m->charset), "iso-8859-2") == 0){
+                len = xtoutf("8859-2", &x, m->body, m->bend);
+                if(len != 0){
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        } else if(cistrcmp(s_to_c(m->charset), "iso-8859-15") == 0){
+                len = xtoutf("8859-15", &x, m->body, m->bend);
+                if(len != 0){
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        } else if(cistrcmp(s_to_c(m->charset), "big5") == 0){
+                len = xtoutf("big5", &x, m->body, m->bend);
+                if(len != 0){
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        } else if(cistrcmp(s_to_c(m->charset), "iso-2022-jp") == 0){
+                len = xtoutf("jis", &x, m->body, m->bend);
+                if(len != 0){
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        } else if(cistrcmp(s_to_c(m->charset), "windows-1257") == 0
+                        || cistrcmp(s_to_c(m->charset), "windows-1252") == 0){
+                len = is8bit(m);
+                if(len > 0){
+                        len = 2*len + m->bend - m->body + 1;
+                        x = emalloc(len);
+                        len = windows1257toutf(x, m->body, m->bend);
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        } else if(cistrcmp(s_to_c(m->charset), "windows-1251") == 0){
+                len = xtoutf("cp1251", &x, m->body, m->bend);
+                if(len != 0){
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        } else if(cistrcmp(s_to_c(m->charset), "koi8-r") == 0){
+                len = xtoutf("koi8", &x, m->body, m->bend);
+                if(len != 0){
+                        if(m->ballocd)
+                                free(m->body);
+                        m->body = x;
+                        m->bend = x + len;
+                        m->ballocd = 1;
+                }
+        }
+
+        m->converted = 1;
+}
+
+enum
+{
+        Self=        1,
+        Hex=        2,
+};
+uchar        tableqp[256];
+
+static void
+initquoted(void)
+{
+        int c;
+
+        memset(tableqp, 0, 256);
+        for(c = ' '; c <= '<'; c++)
+                tableqp[c] = Self;
+        for(c = '>'; c <= '~'; c++)
+                tableqp[c] = Self;
+        tableqp['\t'] = Self;
+        tableqp['='] = Hex;
+}
+
+static int
+hex2int(int x)
+{
+        if(x >= '0' && x <= '9')
+                return x - '0';
+        if(x >= 'A' && x <= 'F')
+                return (x - 'A') + 10;
+        if(x >= 'a' && x <= 'f')
+                return (x - 'a') + 10;
+        return 0;
+}
+
+static char*
+decquotedline(char *out, char *in, char *e)
+{
+        int c, soft;
+
+        /* dump trailing white space */
+        while(e >= in && (*e == ' ' || *e == '\t' || *e == '\r' || *e == '\n'))
+                e--;
+
+        /* trailing '=' means no newline */
+        if(*e == '='){
+                soft = 1;
+                e--;
+        } else
+                soft = 0;
+
+        while(in <= e){
+                c = (*in++) & 0xff;
+                switch(tableqp[c]){
+                case Self:
+                        *out++ = c;
+                        break;
+                case Hex:
+                        c = hex2int(*in++)<<4;
+                        c |= hex2int(*in++);
+                        *out++ = c;
+                        break;
+                }
+        }
+        if(!soft)
+                *out++ = '\n';
+        *out = 0;
+
+        return out;
+}
+
+int
+decquoted(char *out, char *in, char *e)
+{
+        char *p, *nl;
+
+        if(tableqp[' '] == 0)
+                initquoted();
+
+        p = out;
+        while((nl = strchr(in, '\n')) != nil && nl < e){
+                p = decquotedline(p, in, nl);
+                in = nl + 1;
+        }
+        if(in < e)
+                p = decquotedline(p, in, e-1);
+
+        // make sure we end with a new line
+        if(*(p-1) != '\n'){
+                *p++ = '\n';
+                *p = 0;
+        }
+
+        return p - out;
+}
+
+#if 0 /* jpc */
+static char*
+lowercase(char *p)
+{
+        char *op;
+        int c;
+
+        for(op = p; c = *p; p++)
+                if(isupper(c))
+                        *p = tolower(c);
+        return op;
+}
+#endif
+
+/*
+ *  return number of 8 bit characters
+ */
+static int
+is8bit(Message *m)
+{
+        int count = 0;
+        char *p;
+
+        for(p = m->body; p < m->bend; p++)
+                if(*p & 0x80)
+                        count++;
+        return count;
+}
+
+// translate latin1 directly since it fits neatly in utf
+int
+latin1toutf(char *out, char *in, char *e)
+{
+        Rune r;
+        char *p;
+
+        p = out;
+        for(; in < e; in++){
+                r = (*in) & 0xff;
+                p += runetochar(p, &r);
+        }
+        *p = 0;
+        return p - out;
+}
+
+// translate any thing else using the tcs program
+int
+xtoutf(char *charset, char **out, char *in, char *e)
+{
+        char *av[4];
+        int totcs[2];
+        int fromtcs[2];
+        int n, len, sofar;
+        char *p;
+
+        len = e-in+1;
+        sofar = 0;
+        *out = p = malloc(len+1);
+        if(p == nil)
+                return 0;
+
+        av[0] = charset;
+        av[1] = "-f";
+        av[2] = charset;
+        av[3] = 0;
+        if(pipe(totcs) < 0)
+                return 0;
+        if(pipe(fromtcs) < 0){
+                close(totcs[0]); close(totcs[1]);
+                return 0;
+        }
+        switch(rfork(RFPROC|RFFDG|RFNOWAIT)){
+        case -1:
+                close(fromtcs[0]); close(fromtcs[1]);
+                close(totcs[0]); close(totcs[1]);
+                return 0;
+        case 0:
+                close(fromtcs[0]); close(totcs[1]);
+                dup(fromtcs[1], 1);
+                dup(totcs[0], 0);
+                close(fromtcs[1]); close(totcs[0]);
+                dup(open("/dev/null", OWRITE), 2);
+                //jpc exec("/bin/tcs", av);
+                exec(unsharp("#9/bin/tcs"), av);
+                /* _exits(0); */
+                threadexits(nil);
+        default:
+                close(fromtcs[1]); close(totcs[0]);
+                switch(rfork(RFPROC|RFFDG|RFNOWAIT)){
+                case -1:
+                        close(fromtcs[0]); close(totcs[1]);
+                        return 0;
+                case 0:
+                        close(fromtcs[0]);
+                        while(in < e){
+                                n = write(totcs[1], in, e-in);
+                                if(n <= 0)
+                                        break;
+                                in += n;
+                        }
+                        close(totcs[1]);
+                        /* _exits(0); */
+                        threadexits(nil);
+                default:
+                        close(totcs[1]);
+                        for(;;){
+                                n = read(fromtcs[0], &p[sofar], len-sofar);
+                                if(n <= 0)
+                                        break;
+                                sofar += n;
+                                p[sofar] = 0;
+                                if(sofar == len){
+                                        len += 1024;
+                                        *out = p = realloc(p, len+1);
+                                        if(p == nil)
+                                                return 0;
+                                }
+                        }
+                        close(fromtcs[0]);
+                        break;
+                }
+                break;
+        }
+        return sofar;
+}
+
+enum {
+        Winstart= 0x7f,
+        Winend= 0x9f,
+};
+
+Rune winchars[] = {
+        L'•',
+        L'•', L'•', L'‚', L'ƒ', L'„', L'…', L'†', L'‡',
+        L'ˆ', L'‰', L'Š', L'‹', L'Œ', L'•', L'•', L'•',
+        L'•', L'‘', L'’', L'“', L'”', L'•', L'–', L'—',
+        L'˜', L'™', L'š', L'›', L'œ', L'•', L'•', L'Ÿ',
+};
+
+int
+windows1257toutf(char *out, char *in, char *e)
+{
+        Rune r;
+        char *p;
+
+        p = out;
+        for(; in < e; in++){
+                r = (*in) & 0xff;
+                if(r >= 0x7f && r <= 0x9f)
+                        r = winchars[r-0x7f];
+                p += runetochar(p, &r);
+        }
+        *p = 0;
+        return p - out;
+}
+
+void *
+emalloc(ulong n)
+{
+        void *p;
+
+        p = mallocz(n, 1);
+        if(!p){
+                fprint(2, "%s: out of memory alloc %lud\n", argv0, n);
+                threadexits("out of memory");
+        }
+        setmalloctag(p, getcallerpc(&n));
+        return p;
+}
+
+void *
+erealloc(void *p, ulong n)
+{
+        if(n == 0)
+                n = 1;
+        p = realloc(p, n);
+        if(!p){
+                fprint(2, "%s: out of memory realloc %lud\n", argv0, n);
+                threadexits("out of memory");
+        }
+        setrealloctag(p, getcallerpc(&p));
+        return p;
+}
+
+void
+mailplumb(Mailbox *mb, Message *m, int delete)
+{
+        Plumbmsg p;
+        Plumbattr a[7];
+        char buf[256];
+        int ai;
+        char lenstr[10], *from, *subject, *date;
+        static int fd = -1;
+
+        if(m->subject822 == nil)
+                subject = "";
+        else
+                subject = s_to_c(m->subject822);
+
+        if(m->from822 != nil)
+                from = s_to_c(m->from822);
+        else if(m->unixfrom != nil)
+                from = s_to_c(m->unixfrom);
+        else
+                from = "";
+
+        if(m->unixdate != nil)
+                date = s_to_c(m->unixdate);
+        else
+                date = "";
+
+        sprint(lenstr, "%ld", m->end-m->start);
+
+        if(biffing && !delete)
+                print("[ %s / %s / %s ]\n", from, subject, lenstr);
+
+        if(!plumbing)
+                return;
+
+        if(fd < 0)
+                fd = plumbopen("send", OWRITE);
+        if(fd < 0)
+                return;
+
+        p.src = "mailfs";
+        p.dst = "seemail";
+        p.wdir = "/mail/fs";
+        p.type = "text";
+
+        ai = 0;
+        a[ai].name = "filetype";
+        a[ai].value = "mail";
+
+        a[++ai].name = "sender";
+        a[ai].value = from;
+        a[ai-1].next = &a[ai];
+
+        a[++ai].name = "length";
+        a[ai].value = lenstr;
+        a[ai-1].next = &a[ai];
+
+        a[++ai].name = "mailtype";
+        a[ai].value = delete?"delete":"new";
+        a[ai-1].next = &a[ai];
+
+        a[++ai].name = "date";
+        a[ai].value = date;
+        a[ai-1].next = &a[ai];
+
+        if(m->sdigest){
+                a[++ai].name = "digest";
+                a[ai].value = s_to_c(m->sdigest);
+                a[ai-1].next = &a[ai];
+        }
+
+        a[ai].next = nil;
+
+        p.attr = a;
+        snprint(buf, sizeof(buf), "%s/%s/%s",
+                mntpt, mb->name, m->name);
+        p.ndata = strlen(buf);
+        p.data = buf;
+
+        plumbsend(fd, &p);
+}
+
+//
+// count the number of lines in the body (for imap4)
+//
+void
+countlines(Message *m)
+{
+        int i;
+        char *p;
+
+        i = 0;
+        for(p = strchr(m->rbody, '\n'); p != nil && p < m->rbend; p = strchr(p+1, '\n'))
+                i++;
+        sprint(m->lines, "%d", i);
+}
+
+char *LOG = "fs";
+
+void
+logmsg(char *s, Message *m)
+{
+        int pid;
+
+        if(!logging)
+                return;
+        pid = getpid();
+        if(m == nil)
+                syslog(0, LOG, "%s.%d: %s", user, pid, s);
+        else
+                syslog(0, LOG, "%s.%d: %s msg from %s digest %s",
+                        user, pid, s,
+                        m->from822 ? s_to_c(m->from822) : "?",
+                        s_to_c(m->sdigest));
+}
+
+/*
+ *  squeeze nulls out of the body
+ */
+static void
+nullsqueeze(Message *m)
+{
+        char *p, *q;
+
+        q = memchr(m->body, 0, m->end-m->body);
+        if(q == nil)
+                return;
+
+        for(p = m->body; q < m->end; q++){
+                if(*q == 0)
+                        continue;
+                *p++ = *q;
+        }
+        m->bend = m->rbend = m->end = p;
+}
+
+
+//
+// convert an RFC822 date into a Unix style date
+// for when the Unix From line isn't there (e.g. POP3).
+// enough client programs depend on having a Unix date
+// that it's easiest to write this conversion code once, right here.
+//
+// people don't follow RFC822 particularly closely,
+// so we use strtotm, which is a bunch of heuristics.
+//
+
+extern int strtotm(char*, Tm*);
+String*
+date822tounix(char *s)
+{
+        char *p, *q;
+        Tm tm;
+
+        if(strtotm(s, &tm) < 0)
+                return nil;
+
+        p = asctime(&tm);
+        if(q = strchr(p, '\n'))
+                *q = '\0';
+        return s_copy(p);
+}
+
diff --git a/src/cmd/upas/fs/mkfile b/src/cmd/upas/fs/mkfile
t@@ -0,0 +1,29 @@
+<$PLAN9/src/mkhdr
+
+TARG=        fs\
+
+OFILES=\
+        fs.$O\
+        imap4.$O\
+        mbox.$O\
+        plan9.$O\
+        pop3.$O\
+        strtotm.$O\
+
+LIB=../common/libcommon.a\
+#        /usr/local/plan9/lib/libthread.a
+
+HFILES= ../common/common.h\
+        dat.h
+
+BIN=$PLAN9/bin/upas
+
+UPDATE=\
+        mkfile\
+        $HFILES\
+        ${TARG:%=%.c}\
+        ${OFILES:%.$O=%.c}\
+
+<$PLAN9/src/mkone
+CFLAGS=$CFLAGS  -I../common
+# CFLAGS=$CFLAGS -I/sys/include -I../common
diff --git a/src/cmd/upas/fs/mkfile.9 b/src/cmd/upas/fs/mkfile.9
t@@ -0,0 +1,27 @@
+
diff --git a/src/cmd/upas/fs/plan9.c b/src/cmd/upas/fs/plan9.c
t@@ -0,0 +1,405 @@
+#include "common.h"
+#include 
+#include 
+#include 
+#include "dat.h"
+
+enum {
+        Buffersize = 64*1024,
+};
+
+typedef struct Inbuf Inbuf;
+struct Inbuf
+{
+        int        fd;
+        uchar        *lim;
+        uchar        *rptr;
+        uchar        *wptr;
+        uchar        data[Buffersize+7];
+};
+
+static void
+addtomessage(Message *m, uchar *p, int n, int done)
+{
+        int i, len;
+
+        // add to message (+ 1 in malloc is for a trailing null)
+        if(m->lim - m->end < n){
+                if(m->start != nil){
+                        i = m->end-m->start;
+                        if(done)
+                                len = i + n;
+                        else
+                                len = (4*(i+n))/3;
+                        m->start = erealloc(m->start, len + 1);
+                        m->end = m->start + i;
+                } else {
+                        if(done)
+                                len = n;
+                        else
+                                len = 2*n;
+                        m->start = emalloc(len + 1);
+                        m->end = m->start;
+                }
+                m->lim = m->start + len;
+        }
+
+        memmove(m->end, p, n);
+        m->end += n;
+}
+
+//
+//  read in a single message
+//
+static int
+readmessage(Message *m, Inbuf *inb)
+{
+        int i, n, done;
+        uchar *p, *np;
+        char sdigest[SHA1dlen*2+1];
+        char tmp[64];
+
+        for(done = 0; !done;){
+                n = inb->wptr - inb->rptr;
+                if(n < 6){
+                        if(n)
+                                memmove(inb->data, inb->rptr, n);
+                        inb->rptr = inb->data;
+                        inb->wptr = inb->rptr + n;
+                        i = read(inb->fd, inb->wptr, Buffersize);
+                        if(i < 0){
+                                /* if(fd2path(inb->fd, tmp, sizeof tmp) < 0)
+                                        strcpy(tmp, "unknown mailbox");  jpc */
+                                fprint(2, "error reading '%s': %r\n", tmp);
+                                return -1;
+                        }
+                        if(i == 0){
+                                if(n != 0)
+                                        addtomessage(m, inb->rptr, n, 1);
+                                if(m->end == m->start)
+                                        return -1;
+                                break;
+                        }
+                        inb->wptr += i;
+                }
+
+                // look for end of message
+                for(p = inb->rptr; p < inb->wptr; p = np+1){
+                        // first part of search for '\nFrom '
+                        np = memchr(p, '\n', inb->wptr - p);
+                        if(np == nil){
+                                p = inb->wptr;
+                                break;
+                        }
+
+                        /*
+                         *  if we've found a \n but there's
+                         *  not enough room for '\nFrom ', don't do
+                         *  the comparison till we've read in more.
+                         */
+                        if(inb->wptr - np < 6){
+                                p = np;
+                                break;
+                        }
+
+                        if(strncmp((char*)np, "\nFrom ", 6) == 0){
+                                done = 1;
+                                p = np+1;
+                                break;
+                        }
+                }
+
+                // add to message (+ 1 in malloc is for a trailing null)
+                n = p - inb->rptr;
+                addtomessage(m, inb->rptr, n, done);
+                inb->rptr += n;
+        }
+
+        // if it doesn't start with a 'From ', this ain't a mailbox
+        if(strncmp(m->start, "From ", 5) != 0)
+                return -1;
+
+        // dump trailing newline, make sure there's a trailing null
+        // (helps in body searches)
+        if(*(m->end-1) == '\n')
+                m->end--;
+        *m->end = 0;
+        m->bend = m->rbend = m->end;
+
+        // digest message
+        sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
+        for(i = 0; i < SHA1dlen; i++)
+                sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
+        m->sdigest = s_copy(sdigest);
+
+        return 0;
+}
+
+
+// throw out deleted messages.  return number of freshly deleted messages
+int
+purgedeleted(Mailbox *mb)
+{
+        Message *m, *next;
+        int newdels;
+
+        // forget about what's no longer in the mailbox
+        newdels = 0;
+        for(m = mb->root->part; m != nil; m = next){
+                next = m->next;
+                if(m->deleted && m->refs == 0){
+                        if(m->inmbox)
+                                newdels++;
+                        delmessage(mb, m);
+                }
+        }
+        return newdels;
+}
+
+//
+//  read in the mailbox and parse into messages.
+//
+static char*
+_readmbox(Mailbox *mb, int doplumb, Mlock *lk)
+{
+        int fd;
+        String *tmp;
+        Dir *d;
+        static char err[128];
+        Message *m, **l;
+        Inbuf *inb;
+        char *x;
+
+        l = &mb->root->part;
+
+        /*
+         *  open the mailbox.  If it doesn't exist, try the temporary one.
+         */
+retry:
+        fd = open(mb->path, OREAD);
+        if(fd < 0){
+                errstr(err, sizeof(err));
+                if(strstr(err, "exist") != 0){
+                        tmp = s_copy(mb->path);
+                        s_append(tmp, ".tmp");
+                        if(sysrename(s_to_c(tmp), mb->path) == 0){
+                                s_free(tmp);
+                                goto retry;
+                        }
+                        s_free(tmp);
+                }
+                return err;
+        }
+
+        /*
+         *  a new qid.path means reread the mailbox, while
+         *  a new qid.vers means read any new messages
+         */
+        d = dirfstat(fd);
+        if(d == nil){
+                close(fd);
+                errstr(err, sizeof(err));
+                return err;
+        }
+        if(mb->d != nil){
+                if(d->qid.path == mb->d->qid.path && d->qid.vers == mb->d->qid.vers){
+                        close(fd);
+                        free(d);
+                        return nil;
+                }
+                if(d->qid.path == mb->d->qid.path){
+                        while(*l != nil)
+                                l = &(*l)->next;
+                        seek(fd, mb->d->length, 0);
+                }
+                free(mb->d);
+        }
+        mb->d = d;
+        mb->vers++;
+        henter(PATH(0, Qtop), mb->name,
+                (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+
+        inb = emalloc(sizeof(Inbuf));
+        inb->rptr = inb->wptr = inb->data;
+        inb->fd = fd;
+
+        //  read new messages
+        snprint(err, sizeof err, "reading '%s'", mb->path);
+        logmsg(err, nil);
+        for(;;){
+                if(lk != nil)
+                        syslockrefresh(lk);
+                m = newmessage(mb->root);
+                m->mallocd = 1;
+                m->inmbox = 1;
+                if(readmessage(m, inb) < 0){
+                        delmessage(mb, m);
+                        mb->root->subname--;
+                        break;
+                }
+
+                // merge mailbox versions
+                while(*l != nil){
+                        if(memcmp((*l)->digest, m->digest, SHA1dlen) == 0){
+                                // matches mail we already read, discard
+                                logmsg("duplicate", *l);
+                                delmessage(mb, m);
+                                mb->root->subname--;
+                                m = nil;
+                                l = &(*l)->next;
+                                break;
+                        } else {
+                                // old mail no longer in box, mark deleted
+                                logmsg("disappeared", *l);
+                                if(doplumb)
+                                        mailplumb(mb, *l, 1);
+                                (*l)->inmbox = 0;
+                                (*l)->deleted = 1;
+                                l = &(*l)->next;
+                        }
+                }
+                if(m == nil)
+                        continue;
+
+                x = strchr(m->start, '\n');
+                if(x == nil)
+                        m->header = m->end;
+                else
+                        m->header = x + 1;
+                m->mheader = m->mhend = m->header;
+                parseunix(m);
+                parse(m, 0, mb, 0);
+                logmsg("new", m);
+
+                /* chain in */
+                *l = m;
+                l = &m->next;
+                if(doplumb)
+                        mailplumb(mb, m, 0);
+
+        }
+        logmsg("mbox read", nil);
+
+        // whatever is left has been removed from the mbox, mark deleted
+        while(*l != nil){
+                if(doplumb)
+                        mailplumb(mb, *l, 1);
+                (*l)->inmbox = 0;
+                (*l)->deleted = 1;
+                l = &(*l)->next;
+        }
+
+        close(fd);
+        free(inb);
+        return nil;
+}
+
+static void
+_writembox(Mailbox *mb, Mlock *lk)
+{
+        Dir *d;
+        Message *m;
+        String *tmp;
+        int mode, errs;
+        Biobuf *b;
+
+        tmp = s_copy(mb->path);
+        s_append(tmp, ".tmp");
+
+        /*
+         * preserve old files permissions, if possible
+         */
+        d = dirstat(mb->path);
+        if(d != nil){
+                mode = d->mode&0777;
+                free(d);
+        } else
+                mode = MBOXMODE;
+
+        sysremove(s_to_c(tmp));
+        b = sysopen(s_to_c(tmp), "alc", mode);
+        if(b == 0){
+                fprint(2, "can't write temporary mailbox %s: %r\n", s_to_c(tmp));
+                return;
+        }
+
+        logmsg("writing new mbox", nil);
+        errs = 0;
+        for(m = mb->root->part; m != nil; m = m->next){
+                if(lk != nil)
+                        syslockrefresh(lk);
+                if(m->deleted)
+                        continue;
+                logmsg("writing", m);
+                if(Bwrite(b, m->start, m->end - m->start) < 0)
+                        errs = 1;
+                if(Bwrite(b, "\n", 1) < 0)
+                        errs = 1;
+        }
+        logmsg("wrote new mbox", nil);
+
+        if(sysclose(b) < 0)
+                errs = 1;
+
+        if(errs){
+                fprint(2, "error writing temporary mail file\n");
+                s_free(tmp);
+                return;
+        }
+
+        sysremove(mb->path);
+        if(sysrename(s_to_c(tmp), mb->path) < 0)
+                fprint(2, "%s: can't rename %s to %s: %r\n", argv0,
+                        s_to_c(tmp), mb->path);
+        s_free(tmp);
+        if(mb->d != nil)
+                free(mb->d);
+        mb->d = dirstat(mb->path);
+}
+
+char*
+plan9syncmbox(Mailbox *mb, int doplumb)
+{
+        Mlock *lk;
+        char *rv;
+
+        lk = nil;
+        if(mb->dolock){
+                lk = syslock(mb->path);
+                if(lk == nil)
+                        return "can't lock mailbox";
+        }
+
+        rv = _readmbox(mb, doplumb, lk);                /* interpolate */
+        if(purgedeleted(mb) > 0)
+                _writembox(mb, lk);
+
+        if(lk != nil)
+                sysunlock(lk);
+
+        return rv;
+}
+
+//
+//  look to see if we can open this mail box
+//
+char*
+plan9mbox(Mailbox *mb, char *path)
+{
+        static char err[64];
+        String *tmp;
+
+        if(access(path, AEXIST) < 0){
+                errstr(err, sizeof(err));
+                tmp = s_copy(path);
+                s_append(tmp, ".tmp");
+                if(access(s_to_c(tmp), AEXIST) < 0){
+                        s_free(tmp);
+                        return err;
+                }
+                s_free(tmp);
+        }
+
+        mb->sync = plan9syncmbox;
+        return nil;
+}
diff --git a/src/cmd/upas/fs/pop3.c b/src/cmd/upas/fs/pop3.c
t@@ -0,0 +1,700 @@
+#include "common.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "dat.h"
+
+#pragma varargck type "M" uchar*
+#pragma varargck argpos pop3cmd 2
+
+typedef struct Pop Pop;
+struct Pop {
+        char *freep;        // free this to free the strings below
+
+        char *host;
+        char *user;
+        char *port;
+
+        int ppop;
+        int refreshtime;
+        int debug;
+        int pipeline;
+        int encrypted;
+        int needtls;
+        int notls;
+        int needssl;
+
+        // open network connection
+        Biobuf bin;
+        Biobuf bout;
+        int fd;
+        char *lastline;        // from Brdstr
+
+        Thumbprint *thumb;
+};
+
+char*
+geterrstr(void)
+{
+        static char err[64];
+
+        err[0] = '\0';
+        errstr(err, sizeof(err));
+        return err;
+}
+
+//
+// get pop3 response line , without worrying
+// about multiline responses; the clients
+// will deal with that.
+//
+static int
+isokay(char *s)
+{
+        return s!=nil && strncmp(s, "+OK", 3)==0;
+}
+
+static void
+pop3cmd(Pop *pop, char *fmt, ...)
+{
+        char buf[128], *p;
+        va_list va;
+
+        va_start(va, fmt);
+        vseprint(buf, buf+sizeof(buf), fmt, va);
+        va_end(va);
+
+        p = buf+strlen(buf);
+        if(p > (buf+sizeof(buf)-3))
+                sysfatal("pop3 command too long");
+
+        if(pop->debug)
+                fprint(2, "<- %s\n", buf);
+        strcpy(p, "\r\n");
+        Bwrite(&pop->bout, buf, strlen(buf));
+        Bflush(&pop->bout);
+}
+
+static char*
+pop3resp(Pop *pop)
+{
+        char *s;
+        char *p;
+
+        alarm(60*1000);
+        if((s = Brdstr(&pop->bin, '\n', 0)) == nil){
+                close(pop->fd);
+                pop->fd = -1;
+                alarm(0);
+                return "unexpected eof";
+        }
+        alarm(0);
+
+        p = s+strlen(s)-1;
+        while(p >= s && (*p == '\r' || *p == '\n'))
+                *p-- = '\0';
+
+        if(pop->debug)
+                fprint(2, "-> %s\n", s);
+        free(pop->lastline);
+        pop->lastline = s;
+        return s;
+}
+
+#if 0 /* jpc */
+static int
+pop3log(char *fmt, ...)
+{
+        va_list ap;
+
+        va_start(ap,fmt);
+        syslog(0, "/sys/log/pop3", fmt, ap);
+        va_end(ap);
+        return 0;
+}
+#endif
+
+static char*
+pop3pushtls(Pop *pop)
+{
+        int fd;
+        uchar digest[SHA1dlen];
+        TLSconn conn;
+
+        memset(&conn, 0, sizeof conn);
+        // conn.trace = pop3log;
+        fd = tlsClient(pop->fd, &conn);
+        if(fd < 0)
+                return "tls error";
+        if(conn.cert==nil || conn.certlen <= 0){
+                close(fd);
+                return "server did not provide TLS certificate";
+        }
+        sha1(conn.cert, conn.certlen, digest, nil);
+        if(!pop->thumb || !okThumbprint(digest, pop->thumb)){
+                fmtinstall('H', encodefmt);
+                close(fd);
+                free(conn.cert);
+                fprint(2, "upas/fs pop3: server certificate %.*H not recognized\n", SHA1dlen, digest);
+                return "bad server certificate";
+        }
+        free(conn.cert);
+        close(pop->fd);
+        pop->fd = fd;
+        pop->encrypted = 1;
+        Binit(&pop->bin, pop->fd, OREAD);
+        Binit(&pop->bout, pop->fd, OWRITE);
+        return nil;
+}
+
+//
+// get capability list, possibly start tls
+//
+static char*
+pop3capa(Pop *pop)
+{
+        char *s;
+        int hastls;
+
+        pop3cmd(pop, "CAPA");
+        if(!isokay(pop3resp(pop)))
+                return nil;
+
+        hastls = 0;
+        for(;;){
+                s = pop3resp(pop);
+                if(strcmp(s, ".") == 0 || strcmp(s, "unexpected eof") == 0)
+                        break;
+                if(strcmp(s, "STLS") == 0)
+                        hastls = 1;
+                if(strcmp(s, "PIPELINING") == 0)
+                        pop->pipeline = 1;
+        }
+
+        if(hastls && !pop->notls){
+                pop3cmd(pop, "STLS");
+                if(!isokay(s = pop3resp(pop)))
+                        return s;
+                if((s = pop3pushtls(pop)) != nil)
+                        return s;
+        }
+        return nil;
+}
+
+//
+// log in using APOP if possible, password if allowed by user
+//
+static char*
+pop3login(Pop *pop)
+{
+        int n;
+        char *s, *p, *q;
+        char ubuf[128], user[128];
+        char buf[500];
+        UserPasswd *up;
+
+        s = pop3resp(pop);
+        if(!isokay(s))
+                return "error in initial handshake";
+
+        if(pop->user)
+                snprint(ubuf, sizeof ubuf, " user=%q", pop->user);
+        else
+                ubuf[0] = '\0';
+
+        // look for apop banner
+        if(pop->ppop==0 && (p = strchr(s, '<')) && (q = strchr(p+1, '>'))) {
+                *++q = '\0';
+                if((n=auth_respond(p, q-p, user, sizeof user, buf, sizeof buf, auth_getkey, "proto=apop role=client server=%q%s",
+                        pop->host, ubuf)) < 0)
+                        return "factotum failed";
+                if(user[0]=='\0')
+                        return "factotum did not return a user name";
+
+                if(s = pop3capa(pop))
+                        return s;
+
+                pop3cmd(pop, "APOP %s %.*s", user, n, buf);
+                if(!isokay(s = pop3resp(pop)))
+                        return s;
+
+                return nil;
+        } else {
+                if(pop->ppop == 0)
+                        return "no APOP hdr from server";
+
+                if(s = pop3capa(pop))
+                        return s;
+
+                if(pop->needtls && !pop->encrypted)
+                        return "could not negotiate TLS";
+
+                up = auth_getuserpasswd(auth_getkey, "role=client proto=pass service=pop dom=%q%s",
+                        pop->host, ubuf);
+                /* up = auth_getuserpasswd(auth_getkey, "proto=pass service=pop dom=%q%s",
+                        pop->host, ubuf); jpc */
+                if(up == nil)
+                        return "no usable keys found";
+
+                pop3cmd(pop, "USER %s", up->user);
+                if(!isokay(s = pop3resp(pop))){
+                        free(up);
+                        return s;
+                }
+                pop3cmd(pop, "PASS %s", up->passwd);
+                free(up);
+                if(!isokay(s = pop3resp(pop)))
+                        return s;
+
+                return nil;
+        }
+}
+
+//
+// dial and handshake with pop server
+//
+static char*
+pop3dial(Pop *pop)
+{
+        char *err;
+
+        if((pop->fd = dial(netmkaddr(pop->host, "net", pop->needssl ? "pop3s" : "pop3"), 0, 0, 0)) < 0)
+                return geterrstr();
+
+        if(pop->needssl){
+                if((err = pop3pushtls(pop)) != nil)
+                        return err;
+        }else{
+                Binit(&pop->bin, pop->fd, OREAD);
+                Binit(&pop->bout, pop->fd, OWRITE);
+        }
+
+        if(err = pop3login(pop)) {
+                close(pop->fd);
+                return err;
+        }
+
+        return nil;
+}
+
+//
+// close connection
+//
+static void
+pop3hangup(Pop *pop)
+{
+        pop3cmd(pop, "QUIT");
+        pop3resp(pop);
+        close(pop->fd);
+}
+
+//
+// download a single message
+//
+static char*
+pop3download(Pop *pop, Message *m)
+{
+        char *s, *f[3], *wp, *ep;
+        char sdigest[SHA1dlen*2+1];
+        int i, l, sz;
+
+        if(!pop->pipeline)
+                pop3cmd(pop, "LIST %d", m->mesgno);
+        if(!isokay(s = pop3resp(pop)))
+                return s;
+
+        if(tokenize(s, f, 3) != 3)
+                return "syntax error in LIST response";
+
+        if(atoi(f[1]) != m->mesgno)
+                return "out of sync with pop3 server";
+
+        sz = atoi(f[2])+200;        /* 200 because the plan9 pop3 server lies */
+        if(sz == 0)
+                return "invalid size in LIST response";
+
+        m->start = wp = emalloc(sz+1);
+        ep = wp+sz;
+
+        if(!pop->pipeline)
+                pop3cmd(pop, "RETR %d", m->mesgno);
+        if(!isokay(s = pop3resp(pop))) {
+                m->start = nil;
+                free(wp);
+                return s;
+        }
+
+        s = nil;
+        while(wp <= ep) {
+                s = pop3resp(pop);
+                if(strcmp(s, "unexpected eof") == 0) {
+                        free(m->start);
+                        m->start = nil;
+                        return "unexpected end of conversation";
+                }
+                if(strcmp(s, ".") == 0)
+                        break;
+
+                l = strlen(s)+1;
+                if(s[0] == '.') {
+                        s++;
+                        l--;
+                }
+                /*
+                 * grow by 10%/200bytes - some servers
+                 *  lie about message sizes
+                 */
+                if(wp+l > ep) {
+                        int pos = wp - m->start;
+                        sz += ((sz / 10) < 200)? 200: sz/10;
+                        m->start = erealloc(m->start, sz+1);
+                        wp = m->start+pos;
+                        ep = m->start+sz;
+                }
+                memmove(wp, s, l-1);
+                wp[l-1] = '\n';
+                wp += l;
+        }
+
+        if(s == nil || strcmp(s, ".") != 0)
+                return "out of sync with pop3 server";
+
+        m->end = wp;
+
+        // make sure there's a trailing null
+        // (helps in body searches)
+        *m->end = 0;
+        m->bend = m->rbend = m->end;
+        m->header = m->start;
+
+        // digest message
+        sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
+        for(i = 0; i < SHA1dlen; i++)
+                sprint(sdigest+2*i, "%2.2ux", m->digest[i]);
+        m->sdigest = s_copy(sdigest);
+
+        return nil;
+}
+
+//
+// check for new messages on pop server
+// UIDL is not required by RFC 1939, but 
+// netscape requires it, so almost every server supports it.
+// we'll use it to make our lives easier.
+//
+static char*
+pop3read(Pop *pop, Mailbox *mb, int doplumb)
+{
+        char *s, *p, *uidl, *f[2];
+        int mesgno, ignore, nnew;
+        Message *m, *next, **l;
+
+        // Some POP servers disallow UIDL if the maildrop is empty.
+        pop3cmd(pop, "STAT");
+        if(!isokay(s = pop3resp(pop)))
+                return s;
+
+        // fetch message listing; note messages to grab
+        l = &mb->root->part;
+        if(strncmp(s, "+OK 0 ", 6) != 0) {
+                pop3cmd(pop, "UIDL");
+                if(!isokay(s = pop3resp(pop)))
+                        return s;
+
+                for(;;){
+                        p = pop3resp(pop);
+                        if(strcmp(p, ".") == 0 || strcmp(p, "unexpected eof") == 0)
+                                break;
+
+                        if(tokenize(p, f, 2) != 2)
+                                continue;
+
+                        mesgno = atoi(f[0]);
+                        uidl = f[1];
+                        if(strlen(uidl) > 75)        // RFC 1939 says 70 characters max
+                                continue;
+
+                        ignore = 0;
+                        while(*l != nil) {
+                                if(strcmp((*l)->uidl, uidl) == 0) {
+                                        // matches mail we already have, note mesgno for deletion
+                                        (*l)->mesgno = mesgno;
+                                        ignore = 1;
+                                        l = &(*l)->next;
+                                        break;
+                                } else {
+                                        // old mail no longer in box mark deleted
+                                        if(doplumb)
+                                                mailplumb(mb, *l, 1);
+                                        (*l)->inmbox = 0;
+                                        (*l)->deleted = 1;
+                                        l = &(*l)->next;
+                                }
+                        }
+                        if(ignore)
+                                continue;
+
+                        m = newmessage(mb->root);
+                        m->mallocd = 1;
+                        m->inmbox = 1;
+                        m->mesgno = mesgno;
+                        strcpy(m->uidl, uidl);
+
+                        // chain in; will fill in message later
+                        *l = m;
+                        l = &m->next;
+                }
+        }
+
+        // whatever is left has been removed from the mbox, mark as deleted
+        while(*l != nil) {
+                if(doplumb)
+                        mailplumb(mb, *l, 1);
+                (*l)->inmbox = 0;
+                (*l)->deleted = 1;
+                l = &(*l)->next;
+        }
+
+        // download new messages
+        nnew = 0;
+        if(pop->pipeline){
+                switch(rfork(RFPROC|RFMEM)){
+                case -1:
+                        fprint(2, "rfork: %r\n");
+                        pop->pipeline = 0;
+
+                default:
+                        break;
+
+                case 0:
+                        for(m = mb->root->part; m != nil; m = m->next){
+                                if(m->start != nil)
+                                        continue;
+                                Bprint(&pop->bout, "LIST %d\r\nRETR %d\r\n", m->mesgno, m->mesgno);
+                        }
+                        Bflush(&pop->bout);
+                        threadexits(nil);
+                        /* _exits(nil); jpc */
+                }
+        }
+
+        for(m = mb->root->part; m != nil; m = next) {
+                next = m->next;
+
+                if(m->start != nil)
+                        continue;
+
+                if(s = pop3download(pop, m)) {
+                        // message disappeared? unchain
+                        fprint(2, "download %d: %s\n", m->mesgno, s);
+                        delmessage(mb, m);
+                        mb->root->subname--;
+                        continue;
+                }
+                nnew++;
+                parse(m, 0, mb, 1);
+
+                if(doplumb)
+                        mailplumb(mb, m, 0);
+        }
+        if(pop->pipeline)
+                waitpid();
+
+        if(nnew || mb->vers == 0) {
+                mb->vers++;
+                henter(PATH(0, Qtop), mb->name,
+                        (Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+        }
+
+        return nil;        
+}
+
+//
+// delete marked messages
+//
+static void
+pop3purge(Pop *pop, Mailbox *mb)
+{
+        Message *m, *next;
+
+        if(pop->pipeline){
+                switch(rfork(RFPROC|RFMEM)){
+                case -1:
+                        fprint(2, "rfork: %r\n");
+                        pop->pipeline = 0;
+
+                default:
+                        break;
+
+                case 0:
+                        for(m = mb->root->part; m != nil; m = next){
+                                next = m->next;
+                                if(m->deleted && m->refs == 0){
+                                        if(m->inmbox)
+                                                Bprint(&pop->bout, "DELE %d\r\n", m->mesgno);
+                                }
+                        }
+                        Bflush(&pop->bout);
+                        /* _exits(nil); jpc */
+                        threadexits(nil);
+                }
+        }
+        for(m = mb->root->part; m != nil; m = next) {
+                next = m->next;
+                if(m->deleted && m->refs == 0) {
+                        if(m->inmbox) {
+                                if(!pop->pipeline)
+                                        pop3cmd(pop, "DELE %d", m->mesgno);
+                                if(isokay(pop3resp(pop)))
+                                        delmessage(mb, m);
+                        } else
+                                delmessage(mb, m);
+                }
+        }
+}
+
+
+// connect to pop3 server, sync mailbox
+static char*
+pop3sync(Mailbox *mb, int doplumb)
+{
+        char *err;
+        Pop *pop;
+
+        pop = mb->aux;
+
+        if(err = pop3dial(pop)) {
+                mb->waketime = time(0) + pop->refreshtime;
+                return err;
+        }
+
+        if((err = pop3read(pop, mb, doplumb)) == nil){
+                pop3purge(pop, mb);
+                mb->d->atime = mb->d->mtime = time(0);
+        }
+        pop3hangup(pop);
+        mb->waketime = time(0) + pop->refreshtime;
+        return err;
+}
+
+static char Epop3ctl[] = "bad pop3 control message";
+
+static char*
+pop3ctl(Mailbox *mb, int argc, char **argv)
+{
+        int n;
+        Pop *pop;
+        char *m, *me;
+
+        pop = mb->aux;
+        if(argc < 1)
+                return Epop3ctl;
+
+        if(argc==1 && strcmp(argv[0], "debug")==0){
+                pop->debug = 1;
+                return nil;
+        }
+
+        if(argc==1 && strcmp(argv[0], "nodebug")==0){
+                pop->debug = 0;
+                return nil;
+        }
+
+        if(argc==1 && strcmp(argv[0], "thumbprint")==0){
+                if(pop->thumb)
+                        freeThumbprints(pop->thumb);
+                /* pop->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); jpc */
+                m = unsharp("#9/sys/lib/tls/mail");
+                me = unsharp("#9/sys/lib/tls/mail.exclude");
+                pop->thumb = initThumbprints(m, me);
+        }
+        if(strcmp(argv[0], "refresh")==0){
+                if(argc==1){
+                        pop->refreshtime = 60;
+                        return nil;
+                }
+                if(argc==2){
+                        n = atoi(argv[1]);
+                        if(n < 15)
+                                return Epop3ctl;
+                        pop->refreshtime = n;
+                        return nil;
+                }
+        }
+
+        return Epop3ctl;
+}
+
+// free extra memory associated with mb
+static void
+pop3close(Mailbox *mb)
+{
+        Pop *pop;
+
+        pop = mb->aux;
+        free(pop->freep);
+        free(pop);
+}
+
+//
+// open mailboxes of the form /pop/host/user or /apop/host/user
+//
+char*
+pop3mbox(Mailbox *mb, char *path)
+{
+        char *f[10];
+        int nf, apop, ppop, popssl, apopssl, apoptls, popnotls, apopnotls, poptls;
+        Pop *pop;
+        char *m, *me;
+
+        quotefmtinstall();
+        popssl = strncmp(path, "/pops/", 6) == 0;
+        apopssl = strncmp(path, "/apops/", 7) == 0;
+        poptls = strncmp(path, "/poptls/", 8) == 0;
+        popnotls = strncmp(path, "/popnotls/", 10) == 0;
+        ppop = popssl || poptls || popnotls || strncmp(path, "/pop/", 5) == 0;
+        apoptls = strncmp(path, "/apoptls/", 9) == 0;
+        apopnotls = strncmp(path, "/apopnotls/", 11) == 0;
+        apop = apopssl || apoptls || apopnotls || strncmp(path, "/apop/", 6) == 0;
+
+        if(!ppop && !apop)
+                return Enotme;
+
+        path = strdup(path);
+        if(path == nil)
+                return "out of memory";
+
+        nf = getfields(path, f, nelem(f), 0, "/");
+        if(nf != 3 && nf != 4) {
+                free(path);
+                return "bad pop3 path syntax /[a]pop[tls|ssl]/system[/user]";
+        }
+
+        pop = emalloc(sizeof(*pop));
+        pop->freep = path;
+        pop->host = f[2];
+        if(nf < 4)
+                pop->user = nil;
+        else
+                pop->user = f[3];
+        pop->ppop = ppop;
+        pop->needssl = popssl || apopssl;
+        pop->needtls = poptls || apoptls;
+        pop->refreshtime = 60;
+        pop->notls = popnotls || apopnotls;
+        /* pop->thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude"); jpc */
+                m = unsharp("#9/sys/lib/tls/mail");
+                me = unsharp("#9/sys/lib/tls/mail.exclude");
+                pop->thumb = initThumbprints(m, me);
+
+        mb->aux = pop;
+        mb->sync = pop3sync;
+        mb->close = pop3close;
+        mb->ctl = pop3ctl;
+        mb->d = emalloc(sizeof(*mb->d));
+
+        return nil;
+}
+
diff --git a/src/cmd/upas/fs/readdir.c b/src/cmd/upas/fs/readdir.c
t@@ -0,0 +1,15 @@
+#include 
+#include 
+
+void
+main(void)
+{
+        Dir d;
+        int fd, n;
+
+        fd = open("/mail/fs", OREAD);
+        while((n = dirread(fd, &d, sizeof(d))) > 0){
+                print("%s\n", d.name);
+        }
+        print("n = %d\n", n);
+}
diff --git a/src/cmd/upas/fs/strtotm.c b/src/cmd/upas/fs/strtotm.c
t@@ -0,0 +1,113 @@
+#include 
+#include 
+#include 
+
+static char*
+skiptext(char *q)
+{
+        while(*q!='\0' && *q!=' ' && *q!='\t' && *q!='\r' && *q!='\n')
+                q++;
+        return q;
+}
+
+static char*
+skipwhite(char *q)
+{
+        while(*q==' ' || *q=='\t' || *q=='\r' || *q=='\n')
+                q++;
+        return q;
+}
+
+static char* months[] = {
+        "jan", "feb", "mar", "apr",
+        "may", "jun", "jul", "aug", 
+        "sep", "oct", "nov", "dec"
+};
+
+static int
+strcmplwr(char *a, char *b, int n)
+{
+        char *eb;
+
+        eb = b+n;
+        while(*a && *b && b= 1900)
+                                tm.year = j-1900;
+                }
+        }
+
+        if(tm.mon<0 || tm.year<0
+        || tm.hour<0 || tm.min<0
+        || tm.mday<0)
+                return -1;
+
+        *tmp = *localtime(tm2sec(&tm)-delta);
+        return 0;
+}
diff --git a/src/cmd/upas/fs/tester.c b/src/cmd/upas/fs/tester.c
t@@ -0,0 +1,81 @@
+#include 
+#include 
+#include 
+#include 
+#include "message.h"
+
+Message *root;
+
+void
+prindent(int i)
+{
+        for(; i > 0; i--)
+                print(" ");
+}
+
+void
+prstring(int indent, char *tag, String *s)
+{
+        if(s == nil)
+                return;
+        prindent(indent+1);
+        print("%s %s\n", tag, s_to_c(s));
+}
+
+void
+info(int indent, int mno, Message *m)
+{
+        int i;
+        Message *nm;
+
+        prindent(indent);
+        print("%d%c %d ", mno, m->allocated?'*':' ', m->end - m->start);
+        if(m->unixfrom != nil)
+                print("uf %s ", s_to_c(m->unixfrom));
+        if(m->unixdate != nil)
+                print("ud %s ", s_to_c(m->unixdate));
+        print("\n");
+        prstring(indent, "from:", m->from822);
+        prstring(indent, "sender:", m->sender822);
+        prstring(indent, "to:", m->to822);
+        prstring(indent, "cc:", m->cc822);
+        prstring(indent, "reply-to:", m->replyto822);
+        prstring(indent, "subject:", m->subject822);
+        prstring(indent, "date:", m->date822);
+        prstring(indent, "filename:", m->filename);
+        prstring(indent, "type:", m->type);
+        prstring(indent, "charset:", m->charset);
+
+        i = 1;
+        for(nm = m->part; nm != nil; nm = nm->next){
+                info(indent+1, i++, nm);
+        }
+}
+        
+
+void
+main(int argc, char **argv)
+{
+        char *err;
+        char *mboxfile;
+
+        ARGBEGIN{
+        }ARGEND;
+
+        if(argc > 0)
+                mboxfile = argv[0];
+        else
+                mboxfile = "./mbox";
+
+        root = newmessage(nil);
+
+        err = readmbox(mboxfile, &root->part);
+        if(err != nil){
+                fprint(2, "boom: %s\n", err);
+                exits(0);
+        }
+
+        info(0, 1, root);
+
+        exits(0);
+}
diff --git a/src/cmd/upas/marshal/marshal.c b/src/cmd/upas/marshal/marshal.c
t@@ -0,0 +1,1857 @@
+#include "common.h"
+#include 
+
+typedef struct Attach Attach;
+typedef struct Alias Alias;
+typedef struct Addr Addr;
+typedef struct Ctype Ctype;
+
+struct Attach {
+        Attach        *next;
+        char        *path;
+        char        *type;
+        int        tinline;
+        Ctype        *ctype;
+};
+
+struct Alias
+{
+        Alias        *next;
+        int        n;
+        Addr        *addr;
+};
+
+struct Addr
+{
+        Addr        *next;
+        char        *v;
+};
+
+enum {
+        Hfrom,
+        Hto,
+        Hcc,
+        Hbcc,
+        Hsender,
+        Hreplyto,
+        Hinreplyto,
+        Hdate,
+        Hsubject,
+        Hmime,
+        Hpriority,
+        Hmsgid,
+        Hcontent,
+        Hx,
+        Hprecedence,
+        Nhdr,
+};
+
+enum {
+        PGPsign = 1,
+        PGPencrypt = 2,
+};
+
+char *hdrs[Nhdr] = {
+[Hfrom]                "from:",
+[Hto]                "to:",
+[Hcc]                "cc:",
+[Hbcc]                "bcc:",
+[Hreplyto]        "reply-to:",
+[Hinreplyto]        "in-reply-to:",
+[Hsender]        "sender:",
+[Hdate]                "date:",
+[Hsubject]        "subject:",
+[Hpriority]        "priority:",
+[Hmsgid]        "message-id:",
+[Hmime]                "mime-",
+[Hcontent]        "content-",
+[Hx]                "x-",
+[Hprecedence]        "precedence",
+};
+
+struct Ctype {
+        char        *type;
+        char         *ext;
+        int        display;
+};
+
+Ctype ctype[] = {
+        { "text/plain",                        "txt",        1,        },
+        { "text/html",                        "html",        1,        },
+        { "text/html",                        "htm",        1,        },
+        { "text/tab-separated-values",        "tsv",        1,        },
+        { "text/richtext",                "rtx",        1,        },
+        { "message/rfc822",                "txt",        1,        },
+        { "",                                 0,        0,        },
+};
+
+Ctype *mimetypes;
+
+int pid = -1;
+int pgppid = -1;
+
+Attach*        mkattach(char*, char*, int);
+int        readheaders(Biobuf*, int*, String**, Addr**, int);
+void        body(Biobuf*, Biobuf*, int);
+char*        mkboundary(void);
+int        printdate(Biobuf*);
+int        printfrom(Biobuf*);
+int        printto(Biobuf*, Addr*);
+int        printcc(Biobuf*, Addr*);
+int        printsubject(Biobuf*, char*);
+int        printinreplyto(Biobuf*, char*);
+int        sendmail(Addr*, Addr*, int*, char*);
+void        attachment(Attach*, Biobuf*);
+int        cistrncmp(char*, char*, int);
+int        cistrcmp(char*, char*);
+char*        waitforsubprocs(void);
+int        enc64(char*, int, uchar*, int);
+Addr*        expand(int, char**);
+Alias*        readaliases(void);
+Addr*        expandline(String**, Addr*);
+void        Bdrain(Biobuf*);
+void        freeaddr(Addr *);
+int        pgpopts(char*);
+int        pgpfilter(int*, int, int);
+void        readmimetypes(void);
+char*        estrdup(char*);
+void*        emalloc(int);
+void*        erealloc(void*, int);
+void        freeaddr(Addr*);
+void        freeaddrs(Addr*);
+void        freealias(Alias*);
+void        freealiases(Alias*);
+int        doublequote(Fmt*);
+
+int rflag, lbflag, xflag, holding, nflag, Fflag, eightflag, dflag;
+int pgpflag = 0;
+char *user;
+char *login;
+Alias *aliases;
+int rfc822syntaxerror;
+char lastchar;
+char *replymsg;
+
+enum
+{
+        Ok = 0,
+        Nomessage = 1,
+        Nobody = 2,
+        Error = -1,
+};
+
+#pragma varargck        type        "Z"        char*
+
+void
+usage(void)
+{
+        fprint(2, "usage: %s [-Fr#xn] [-s subject] [-c ccrecipient] [-t type] [-aA attachment] [-p[es]] [-R replymsg] -8 | recipient-list\n",
+                argv0);
+        exits("usage");
+}
+
+void
+fatal(char *fmt, ...)
+{
+        char buf[1024];
+        va_list arg;
+
+        if(pid >= 0)
+                postnote(PNPROC, pid, "die");
+        if(pgppid >= 0)
+                postnote(PNPROC, pgppid, "die");
+
+        va_start(arg, fmt);
+        vseprint(buf, buf+sizeof(buf), fmt, arg);
+        va_end(arg);
+        fprint(2, "%s: %s\n", argv0, buf);
+        holdoff(holding);
+        exits(buf);
+}
+
+void
+main(int argc, char **argv)
+{
+        Attach *first, **l, *a;
+        char *subject, *type, *boundary;
+        int flags, fd;
+        Biobuf in, out, *b;
+        Addr *to;
+        Addr *cc;
+        String *file, *hdrstring;
+        int noinput, headersrv;
+        int ccargc;
+        char *ccargv[32];
+
+        noinput = 0;
+        subject = nil;
+        first = nil;
+        l = &first;
+        type = nil;
+        hdrstring = nil;
+        ccargc = 0;
+
+        quotefmtinstall();
+        fmtinstall('Z', doublequote);
+
+        ARGBEGIN{
+        case 't':
+                type = ARGF();
+                if(type == nil)
+                        usage();
+                break;
+        case 'a':
+                flags = 0;
+                goto aflag;
+        case 'A':
+                flags = 1;
+        aflag:
+                a = mkattach(ARGF(), type, flags);
+                if(a == nil)
+                        exits("bad args");
+                type = nil;
+                *l = a;
+                l = &a->next;
+                break;
+        case 'C':
+                if(ccargc >= nelem(ccargv)-1)
+                        sysfatal("too many cc's");
+                ccargv[ccargc] = ARGF();
+                if(ccargv[ccargc] == nil)
+                        usage();
+                ccargc++;
+                break;
+        case 'R':
+                replymsg = ARGF();
+                break;
+        case 's':
+                subject = ARGF();
+                break;
+        case 'F':
+                Fflag = 1;                // file message
+                break;
+        case 'r':
+                rflag = 1;                // for sendmail
+                break;
+        case 'd':
+                dflag = 1;                // for sendmail
+                break;
+        case '#':
+                lbflag = 1;                // for sendmail
+                break;
+        case 'x':
+                xflag = 1;                // for sendmail
+                break;
+        case 'n':                        // no standard input
+                nflag = 1;
+                break;
+        case '8':                        // read recipients from rfc822 header
+                eightflag = 1;
+                break;
+        case 'p':                        // pgp flag: encrypt, sign, or both
+                if(pgpopts(ARGF()) < 0)
+                        sysfatal("bad pgp options");
+                break;
+        default:
+                usage();
+                break;
+        }ARGEND;
+
+        login = getlog();
+        user = getenv("upasname");
+        if(user == nil || *user == 0)
+                user = login;
+        if(user == nil || *user == 0)
+                sysfatal("can't read user name");
+
+        if(Binit(&in, 0, OREAD) < 0)
+                sysfatal("can't Binit 0: %r");
+
+        if(nflag && eightflag)
+                sysfatal("can't use both -n and -8");
+        if(eightflag && argc >= 1)
+                usage();
+        else if(!eightflag && argc < 1)
+                usage();
+
+        aliases = readaliases();
+        if(!eightflag){
+                to = expand(argc, argv);
+                cc = expand(ccargc, ccargv);
+        } else {
+                to = nil;
+                cc = nil;
+        }
+
+        flags = 0;
+        headersrv = Nomessage;
+        if(!nflag && !xflag && !lbflag &&!dflag) {
+                // pass through headers, keeping track of which we've seen,
+                // perhaps building to list.
+                holding = holdon();
+                headersrv = readheaders(&in, &flags, &hdrstring, eightflag ? &to : nil, 1);
+                if(rfc822syntaxerror){
+                        Bdrain(&in);
+                        fatal("rfc822 syntax error, message not sent");
+                }
+                if(to == nil){
+                        Bdrain(&in);
+                        fatal("no addresses found, message not sent");
+                }
+
+                switch(headersrv){
+                case Error:                // error
+                        fatal("reading");
+                        break;
+                case Nomessage:                // no message, just exit mimicking old behavior
+                        noinput = 1;
+                        if(first == nil)
+                                exits(0);
+                        break;
+                }
+        }
+
+        fd = sendmail(to, cc, &pid, Fflag ? argv[0] : nil);
+        if(fd < 0)
+                sysfatal("execing sendmail: %r\n:");
+        if(xflag || lbflag || dflag){
+                close(fd);
+                exits(waitforsubprocs());
+        }
+        
+        if(Binit(&out, fd, OWRITE) < 0)
+                fatal("can't Binit 1: %r");
+
+        if(!nflag){
+                if(Bwrite(&out, s_to_c(hdrstring), s_len(hdrstring)) != s_len(hdrstring))
+                        fatal("write error");
+                s_free(hdrstring);
+                hdrstring = nil;
+
+                // read user's standard headers
+                file = s_new();
+                mboxpath("headers", user, file, 0);
+                b = Bopen(s_to_c(file), OREAD);
+                if(b != nil){
+                        switch(readheaders(b, &flags, &hdrstring, nil, 0)){
+                        case Error:        // error
+                                fatal("reading");
+                        }
+                        Bterm(b);
+                        if(Bwrite(&out, s_to_c(hdrstring), s_len(hdrstring)) != s_len(hdrstring))
+                                fatal("write error");
+                        s_free(hdrstring);
+                        hdrstring = nil;
+                }
+        }
+
+        // add any headers we need
+        if((flags & (1<next){
+                if(lastchar != '\n')
+                        Bprint(&out, "\n");
+                Bprint(&out, "--%s\n", boundary);
+                attachment(a, &out);
+        }
+
+        if(first != nil){
+                if(lastchar != '\n')
+                        Bprint(&out, "\n");
+                Bprint(&out, "--%s--\n", boundary);
+        }
+
+        Bterm(&out);
+        close(fd);
+        exits(waitforsubprocs());
+}
+
+// evaluate pgp option string
+int
+pgpopts(char *s)
+{
+        if(s == nil || s[0] == '\0')
+                return -1;
+        while(*s){
+                switch(*s++){
+                case 's':  case 'S':
+                        pgpflag |= PGPsign;
+                        break;
+                case 'e': case 'E':
+                        pgpflag |= PGPencrypt;
+                        break;
+                default:
+                        return -1;
+                }
+        }
+        return 0;
+}
+
+// read headers from stdin into a String, expanding local aliases,
+// keep track of which headers are there, which addresses we have
+// remove Bcc: line.
+int
+readheaders(Biobuf *in, int *fp, String **sp, Addr **top, int strict)
+{
+        Addr *to;
+        String *s, *sline;
+        char *p;
+        int i, seen, hdrtype;
+
+        s = s_new();
+        sline = nil;
+        to = nil;
+        hdrtype = -1;
+        seen = 0;
+        for(;;) {
+                if((p = Brdline(in, '\n')) != nil) {
+                        seen = 1;
+                        p[Blinelen(in)-1] = 0;
+
+                        // coalesce multiline headers
+                        if((*p == ' ' || *p == '\t') && sline){
+                                s_append(sline, "\n");
+                                s_append(sline, p);
+                                p[Blinelen(in)-1] = '\n';
+                                continue;
+                        }
+                }
+
+                // process the current header, it's all been read
+                if(sline) {
+                        assert(hdrtype != -1);
+                        if(top){
+                                switch(hdrtype){
+                                case Hto:
+                                case Hcc:
+                                case Hbcc:
+                                        to = expandline(&sline, to);
+                                        break;
+                                }
+                        }
+                        if(top==nil || hdrtype!=Hbcc){
+                                s_append(s, s_to_c(sline));
+                                s_append(s, "\n");
+                        }
+                        s_free(sline);
+                        sline = nil;
+                }
+
+                if(p == nil)
+                        break;
+
+                // if no :, it's not a header, seek back and break
+                if(strchr(p, ':') == nil){
+                        p[Blinelen(in)-1] = '\n';
+                        Bseek(in, -Blinelen(in), 1);
+                        break;
+                }
+
+                sline = s_copy(p);
+
+                // classify the header.  If we don't recognize it, break.  This is
+                // to take care of user's that start messages with lines that contain
+                // ':'s but that aren't headers.  This is a bit hokey.  Since I decided
+                // to let users type headers, I need some way to distinguish.  Therefore,
+                // marshal tries to know all likely headers and will indeed screw up if
+                // the user types an unlikely one. -- presotto
+                hdrtype = -1;
+                for(i = 0; i < nelem(hdrs); i++){
+                        if(cistrncmp(hdrs[i], p, strlen(hdrs[i])) == 0){
+                                *fp |= 1< 0){
+                if(i != '\n')
+                        buf[n++] = '\n';
+                buf[n++] = i;
+        } else {
+                buf[n++] = '\n';
+        }
+
+        // read into memory
+        if(docontenttype){
+                while(docontenttype){
+                        if(n == len){
+                                len += len>>2;
+                                buf = realloc(buf, len);
+                                if(buf == nil)
+                                        sysfatal("%r");
+                        }
+                        p = buf+n;
+                        i = Bread(in, p, len - n);
+                        if(i < 0)
+                                fatal("input error2");
+                        if(i == 0)
+                                break;
+                        n += i;
+                        for(; i > 0; i--)
+                                if((*p++ & 0x80) && docontenttype){
+                                        Bprint(out, "Content-Type: text/plain; charset=\"UTF-8\"\n");
+                                        Bprint(out, "Content-Transfer-Encoding: 8bit\n");
+                                        docontenttype = 0;
+                                        break;
+                                }
+                }
+                if(docontenttype){
+                        Bprint(out, "Content-Type: text/plain; charset=\"US-ASCII\"\n");
+                        Bprint(out, "Content-Transfer-Encoding: 7bit\n");
+                }
+        }
+
+        // write what we already read
+        if(Bwrite(out, buf, n) < 0)
+                fatal("output error");
+        if(n > 0)
+                lastchar = buf[n-1];
+        else
+                lastchar = '\n';
+
+
+        // pass the rest
+        for(;;){
+                n = Bread(in, buf, len);
+                if(n < 0)
+                        fatal("input error2");
+                if(n == 0)
+                        break;
+                if(Bwrite(out, buf, n) < 0)
+                        fatal("output error");
+                lastchar = buf[n-1];
+        }
+}
+
+// pass the body to sendmail encoding with base64
+//
+//  the size of buf is very important to enc64.  Anything other than
+//  a multiple of 3 will cause enc64 to output a termination sequence.
+//  To ensure that a full buf corresponds to a multiple of complete lines,
+//  we make buf a multiple of 3*18 since that's how many enc64 sticks on
+//  a single line.  This avoids short lines in the output which is pleasing
+//  but not necessary.
+//
+void
+body64(Biobuf *in, Biobuf *out)
+{
+        uchar buf[3*18*54];
+        char obuf[3*18*54*2];
+        int m, n;
+
+        Bprint(out, "\n");
+        for(;;){
+                n = Bread(in, buf, sizeof(buf));
+                fprint(2,"read %d bytes\n",n);
+                if(n < 0)
+                        fatal("input error");
+                if(n == 0)
+                        break;
+                m = enc64(obuf, sizeof(obuf), buf, n);
+                fprint(2,"encoded %d bytes\n",m);
+                fprint(2,"writing to %x\n",out);
+                if((n=Bwrite(out, obuf, m)) < 0)
+                        fatal("output error");
+                fprint(2,"wrote %d bytes\n",n);
+        }
+        lastchar = '\n';
+                fprint(2,"done with attachment\n");
+}
+
+// pass message to sendmail, make sure body starts with a newline
+void
+copy(Biobuf *in, Biobuf *out)
+{
+        char buf[4*1024];
+        int n;
+
+        for(;;){
+                n = Bread(in, buf, sizeof(buf));
+                if(n < 0)
+                        fatal("input error");
+                if(n == 0)
+                        break;
+                if(Bwrite(out, buf, n) < 0)
+                        fatal("output error");
+        }
+}
+
+void
+attachment(Attach *a, Biobuf *out)
+{
+        Biobuf *f;
+        char *p;
+
+        // if it's already mime encoded, just copy
+        if(strcmp(a->type, "mime") == 0){
+                f = Bopen(a->path, OREAD);
+                if(f == nil){
+                        /* hack: give marshal time to stdin, before we kill it (for dead.letter) */
+                        sleep(500);
+                        postnote(PNPROC, pid, "interrupt");
+                        sysfatal("opening %s: %r", a->path);
+                }
+                copy(f, out);
+                Bterm(f);
+        }
+        
+        // if it's not already mime encoded ...
+        if(strcmp(a->type, "text/plain") != 0)
+                Bprint(out, "Content-Type: %s\n", a->type);
+
+        if(a->tinline){
+                Bprint(out, "Content-Disposition: inline\n");
+        } else {
+                p = strrchr(a->path, '/');
+                if(p == nil)
+                        p = a->path;
+                else
+                        p++;
+                Bprint(out, "Content-Disposition: attachment; filename=%Z\n", p);
+        }
+
+        f = Bopen(a->path, OREAD);
+        if(f == nil){
+                /* hack: give marshal time to stdin, before we kill it (for dead.letter) */
+                sleep(500);
+                postnote(PNPROC, pid, "interrupt");
+                sysfatal("opening %s: %r", a->path);
+        }
+
+        /* dump our local 'From ' line when passing along mail messages */
+        if(strcmp(a->type, "message/rfc822") == 0){
+                p = Brdline(f, '\n');
+                if(strncmp(p, "From ", 5) != 0)
+                        Bseek(f, 0, 0);
+        }
+        if(a->ctype->display){
+                body(f, out, strcmp(a->type, "text/plain") == 0);
+        } else {
+                Bprint(out, "Content-Transfer-Encoding: base64\n");
+                body64(f, out);
+        }
+        Bterm(f);
+}
+
+char *ascwday[] =
+{
+        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+char *ascmon[] =
+{
+        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+int
+printdate(Biobuf *b)
+{
+        Tm *tm;
+        int tz;
+
+        tm = localtime(time(0));
+        tz = (tm->tzoff/3600)*100 + ((tm->tzoff/60)%60);
+
+        return Bprint(b, "Date: %s, %d %s %d %2.2d:%2.2d:%2.2d %s%.4d\n",
+                ascwday[tm->wday], tm->mday, ascmon[tm->mon], 1900+tm->year,
+                tm->hour, tm->min, tm->sec, tz>=0?"+":"", tz);
+}
+
+int
+printfrom(Biobuf *b)
+{
+        return Bprint(b, "From: %s\n", user);
+}
+
+int
+printto(Biobuf *b, Addr *a)
+{
+        int i;
+
+        if(Bprint(b, "To: %s", a->v) < 0)
+                return -1;
+        i = 0;
+        for(a = a->next; a != nil; a = a->next)
+                if(Bprint(b, "%s%s", ((i++ & 7) == 7)?",\n\t":", ", a->v) < 0)
+                        return -1;
+        if(Bprint(b, "\n") < 0)
+                return -1;
+        return 0;
+}
+
+int
+printcc(Biobuf *b, Addr *a)
+{
+        int i;
+
+        if(a == nil)
+                return 0;
+        if(Bprint(b, "CC: %s", a->v) < 0)
+                return -1;
+        i = 0;
+        for(a = a->next; a != nil; a = a->next)
+                if(Bprint(b, "%s%s", ((i++ & 7) == 7)?",\n\t":", ", a->v) < 0)
+                        return -1;
+        if(Bprint(b, "\n") < 0)
+                return -1;
+        return 0;
+}
+
+int
+printsubject(Biobuf *b, char *subject)
+{
+        return Bprint(b, "Subject: %s\n", subject);
+}
+
+int
+printinreplyto(Biobuf *out, char *dir)
+{
+        String *s = s_copy(dir);
+        char buf[256];
+        int fd;
+        int n;
+
+        s_append(s, "/messageid");
+        fd = open(s_to_c(s), OREAD);
+        s_free(s);
+        if(fd < 0)
+                return 0;
+        n = read(fd, buf, sizeof(buf)-1);
+        close(fd);
+        if(n <= 0)
+                return 0;
+        buf[n] = 0;
+        return Bprint(out, "In-Reply-To: %s\n", buf);
+}
+
+Attach*
+mkattach(char *file, char *type, int tinline)
+{
+        Ctype *c;
+        Attach *a;
+        char ftype[64];
+        char *p;
+        int n, pfd[2];
+
+        if(file == nil)
+                return nil;
+        if(access(file, 4) == -1){
+                fprint(2, "%s: %s can't read file\n", argv0, file);
+                return nil;
+        }
+        a = emalloc(sizeof(*a));
+        a->path = file;
+        a->next = nil;
+        a->type = type;
+        a->tinline = tinline;
+        a->ctype = nil;
+        if(type != nil){
+                for(c = ctype; ; c++)
+                        if(strncmp(type, c->type, strlen(c->type)) == 0){
+                                a->ctype = c;
+                                break;
+                        }
+                return a;
+        }
+
+        // pick a type depending on extension
+        p = strchr(file, '.');
+        if(p != nil)
+                p++;
+
+        // check the builtin extensions
+        if(p != nil){
+                for(c = ctype; c->ext != nil; c++)
+                        if(strcmp(p, c->ext) == 0){
+                                a->type = c->type;
+                                a->ctype = c;
+                                return a;
+                        }
+        }
+
+        // try the mime types file
+        if(p != nil){
+                if(mimetypes == nil)
+                        readmimetypes();
+                for(c = mimetypes; c != nil && c->ext != nil; c++)
+                        if(strcmp(p, c->ext) == 0){
+                                a->type = c->type;
+                                a->ctype = c;
+                                return a;
+                        }
+        }
+
+        // run file to figure out the type
+        a->type = "application/octet-stream";                // safest default
+        if(pipe(pfd) < 0)
+                return a;
+        switch(fork()){
+        case -1:
+                break;
+        case 0:
+                close(pfd[1]);
+                close(0);
+                dup(pfd[0], 0);
+                close(1);
+                dup(pfd[0], 1);
+                execl(unsharp("#9/bin/file"), "file", "-m", file, nil);
+                exits(0);
+        default:
+                close(pfd[0]);
+                n = read(pfd[1], ftype, sizeof(ftype));
+                if(n > 0){
+                        ftype[n-1] = 0;
+                        a->type = estrdup(ftype);
+                }
+                close(pfd[1]);
+                waitpid();
+                break;
+        }
+
+        for(c = ctype; ; c++)
+                if(strncmp(a->type, c->type, strlen(c->type)) == 0){
+                        a->ctype = c;
+                        break;
+                }
+
+        return a;
+}
+
+char*
+mkboundary(void)
+{
+        char buf[32];
+        int i;
+
+        srand((time(0)<<16)|getpid());
+        strcpy(buf, "upas-");
+        for(i = 5; i < sizeof(buf)-1; i++)
+                buf[i] = 'a' + nrand(26);
+        buf[i] = 0;
+        return estrdup(buf);
+}
+
+// copy types to two fd's
+static void
+tee(int in, int out1, int out2)
+{
+        char buf[8*1024];
+        int n;
+
+        for(;;){
+                n = read(in, buf, sizeof(buf));
+                if(n <= 0)
+                        break;
+                if(write(out1, buf, n) < 0)
+                        break;
+                if(write(out2, buf, n) < 0)
+                        break;
+        }
+}
+
+// print the unix from line
+int
+printunixfrom(int fd)
+{
+        Tm *tm;
+        int tz;
+
+        tm = localtime(time(0));
+        tz = (tm->tzoff/3600)*100 + ((tm->tzoff/60)%60);
+
+        return fprint(fd, "From %s %s %s %d %2.2d:%2.2d:%2.2d %s%.4d %d\n",
+                user,
+                ascwday[tm->wday], ascmon[tm->mon], tm->mday,
+                tm->hour, tm->min, tm->sec, tz>=0?"+":"", tz, 1900+tm->year);
+}
+
+char *specialfile[] =
+{
+        "pipeto",
+        "pipefrom",
+        "L.mbox",
+        "forward",
+        "names"
+};
+
+// return 1 if this is a special file
+static int
+special(String *s)
+{
+        char *p;
+        int i;
+
+        p = strrchr(s_to_c(s), '/');
+        if(p == nil)
+                p = s_to_c(s);
+        else
+                p++;
+        for(i = 0; i < nelem(specialfile); i++)
+                if(strcmp(p, specialfile[i]) == 0)
+                        return 1;
+        return 0;
+}
+
+// open the folder using the recipients account name
+static int
+openfolder(char *rcvr)
+{
+        char *p;
+        int c;
+        String *file;
+        Dir *d;
+        int fd;
+        int scarey;
+
+        file = s_new();
+        mboxpath("f", user, file, 0);
+
+        // if $mail/f exists, store there, otherwise in $mail
+        d = dirstat(s_to_c(file));
+        if(d == nil || d->qid.type != QTDIR){
+                scarey = 1;
+                file->ptr -= 1;
+        } else {
+                s_putc(file, '/');
+                scarey = 0;
+        }
+        free(d);
+
+        p = strrchr(rcvr, '!');
+        if(p != nil)
+                rcvr = p+1;
+
+        while(*rcvr && *rcvr != '@'){
+                c = *rcvr++;
+                if(c == '/')
+                        c = '_';
+                s_putc(file, c);
+        }
+        s_terminate(file);
+
+        if(scarey && special(file)){
+                fprint(2, "%s: won't overwrite %s\n", argv0, s_to_c(file));
+                s_free(file);
+                return -1;
+        }
+
+        fd = open(s_to_c(file), OWRITE);
+        if(fd < 0)
+                fd = create(s_to_c(file), OWRITE, 0660);
+
+        s_free(file);
+        return fd;
+}
+
+// start up sendmail and return an fd to talk to it with
+int
+sendmail(Addr *to, Addr *cc, int *pid, char *rcvr)
+{
+        char **av, **v;
+        int ac, fd;
+        int pfd[2];
+        String *cmd;
+        Addr *a;
+
+        fd = -1;
+        if(rcvr != nil)
+                fd = openfolder(rcvr);
+
+        ac = 0;
+        for(a = to; a != nil; a = a->next)
+                ac++;
+        for(a = cc; a != nil; a = a->next)
+                ac++;
+        v = av = emalloc(sizeof(char*)*(ac+20));
+        ac = 0;
+        v[ac++] = "sendmail";
+        if(xflag)
+                v[ac++] = "-x";
+        if(rflag)
+                v[ac++] = "-r";
+        if(lbflag)
+                v[ac++] = "-#";
+        if(dflag)
+                v[ac++] = "-d";
+        for(a = to; a != nil; a = a->next)
+                v[ac++] = a->v;
+        for(a = cc; a != nil; a = a->next)
+                v[ac++] = a->v;
+        v[ac] = 0;
+
+        if(pipe(pfd) < 0)
+                fatal("%r");
+        switch(*pid = rfork(RFFDG|RFPROC)){   // jpc - removed |RFENVG|RFREND|
+        case -1:
+                fatal("%r");
+                break;
+        case 0:
+                if(holding)
+                        close(holding);
+                close(pfd[1]);
+                dup(pfd[0], 0);
+                close(pfd[0]);
+
+                if(rcvr != nil){
+                        if(pipe(pfd) < 0)
+                                fatal("%r");
+                        switch(fork()){
+                        case -1:
+                                fatal("%r");
+                                break;
+                        case 0:
+                                close(pfd[0]);
+                                seek(fd, 0, 2);
+                                printunixfrom(fd);
+                                tee(0, pfd[1], fd);
+                                write(fd, "\n", 1);
+                                exits(0);
+                        default:
+                                close(fd);
+                                close(pfd[1]);
+                                dup(pfd[0], 0);
+                                break;
+                        }
+                }
+
+                if(replymsg != nil)
+                        putenv("replymsg", replymsg);
+
+                cmd = mboxpath("pipefrom", login, s_new(), 0);
+                exec(s_to_c(cmd), av);
+                exec(unsharp("#9/bin/myupassend"), av);
+                exec(unsharp("#9/bin/upas/send"), av);
+                fatal("execing: %r");
+                break;
+        default:
+                if(rcvr != nil)
+                        close(fd);
+                close(pfd[0]);
+                break;
+        }
+        return pfd[1];
+}
+
+// start up pgp process and return an fd to talk to it with.
+// its standard output will be the original fd, which goes to sendmail.
+int
+pgpfilter(int *pid, int fd, int pgpflag)
+{
+        char **av, **v;
+        int ac;
+        int pfd[2];
+
+        v = av = emalloc(sizeof(char*)*8);
+        ac = 0;
+        v[ac++] = "pgp";
+        if(pgpflag & PGPsign)
+                v[ac++] = "-s";
+        if(pgpflag & PGPencrypt)
+                v[ac++] = "-e";
+        v[ac] = 0;
+
+        if(pipe(pfd) < 0)
+                fatal("%r");
+        switch(*pid = fork()){
+        case -1:
+                fatal("%r");
+                break;
+        case 0:
+                close(pfd[1]);
+                dup(pfd[0], 0);
+                close(pfd[0]);
+                dup(fd, 1);
+                close(fd);
+
+                exec("/bin/upas/pgp", av);
+                fatal("execing: %r");
+                break;
+        default:
+                close(pfd[0]);
+                break;
+        }
+        close(fd);
+        return pfd[1];
+}
+
+// wait for sendmail and pgp to exit; exit here if either failed
+char*
+waitforsubprocs(void)
+{
+        Waitmsg *w;
+        char *err;
+
+        err = nil;
+        while((w = wait()) != nil){
+                if(w->pid == pid || w->pid == pgppid){
+                        if(w->msg[0] != 0)
+                                err = estrdup(w->msg);
+                }
+                free(w);
+        }
+        if(err)
+                exits(err);
+        return nil;
+}
+
+int
+cistrncmp(char *a, char *b, int n)
+{
+        while(n-- > 0){
+                if(tolower(*a++) != tolower(*b++))
+                        return -1;
+        }
+        return 0;
+}
+
+int
+cistrcmp(char *a, char *b)
+{
+        for(;;){
+                if(tolower(*a) != tolower(*b++))
+                        return -1;
+                if(*a++ == 0)
+                        break;
+        }
+        return 0;
+}
+
+static uchar t64d[256];
+static char t64e[64];
+
+static void
+init64(void)
+{
+        int c, i;
+
+        memset(t64d, 255, 256);
+        memset(t64e, '=', 64);
+        i = 0;
+        for(c = 'A'; c <= 'Z'; c++){
+                t64e[i] = c;
+                t64d[c] = i++;
+        }
+        for(c = 'a'; c <= 'z'; c++){
+                t64e[i] = c;
+                t64d[c] = i++;
+        }
+        for(c = '0'; c <= '9'; c++){
+                t64e[i] = c;
+                t64d[c] = i++;
+        }
+        t64e[i] = '+';
+        t64d['+'] = i++;
+        t64e[i] = '/';
+        t64d['/'] = i;
+}
+
+int
+enc64(char *out, int lim, uchar *in, int n)
+{
+        int i;
+        ulong b24;
+        char *start = out;
+        char *e = out + lim;
+
+        if(t64e[0] == 0)
+                init64();
+        for(i = 0; i < n/3; i++){
+                b24 = (*in++)<<16;
+                b24 |= (*in++)<<8;
+                b24 |= *in++;
+                if(out + 5 >= e)
+                        goto exhausted;
+                *out++ = t64e[(b24>>18)];
+                *out++ = t64e[(b24>>12)&0x3f];
+                *out++ = t64e[(b24>>6)&0x3f];
+                *out++ = t64e[(b24)&0x3f];
+                if((i%18) == 17)
+                        *out++ = '\n';
+        }
+
+        switch(n%3){
+        case 2:
+                b24 = (*in++)<<16;
+                b24 |= (*in)<<8;
+                if(out + 4 >= e)
+                        goto exhausted;
+                *out++ = t64e[(b24>>18)];
+                *out++ = t64e[(b24>>12)&0x3f];
+                *out++ = t64e[(b24>>6)&0x3f];
+                break;
+        case 1:
+                b24 = (*in)<<16;
+                if(out + 4 >= e)
+                        goto exhausted;
+                *out++ = t64e[(b24>>18)];
+                *out++ = t64e[(b24>>12)&0x3f];
+                *out++ = '=';
+                break;
+        case 0:
+                if((i%18) != 0)
+                        *out++ = '\n';
+                *out = 0;
+                return out - start;
+        }
+exhausted:
+        *out++ = '=';
+        *out++ = '\n';
+        *out = 0;
+        return out - start;
+}
+
+void
+freealias(Alias *a)
+{
+        freeaddrs(a->addr);
+        free(a);
+}
+
+void
+freealiases(Alias *a)
+{
+        Alias *next;
+
+        while(a != nil){
+                next = a->next;
+                freealias(a);
+                a = next;
+        }
+}
+
+//
+//  read alias file
+//
+Alias*
+readaliases(void)
+{
+        Alias *a, **l, *first;
+        Addr *addr, **al;
+        String *file, *line, *token;
+        // jpc - static int already;
+        Sinstack *sp;
+
+        first = nil;
+        file = s_new();
+        line = s_new();
+        token = s_new();
+
+        // open and get length
+        mboxpath("names", login, file, 0);
+        sp = s_allocinstack(s_to_c(file));
+        if(sp == nil)
+                goto out;
+
+        l = &first;
+
+        // read a line at a time.
+        while(s_rdinstack(sp, s_restart(line))!=nil) {
+                s_restart(line);
+                a = emalloc(sizeof(Alias));
+                al = &a->addr;
+                for(;;){
+                        if(s_parse(line, s_restart(token))==0)
+                                break;
+                        addr = emalloc(sizeof(Addr));
+                        addr->v = strdup(s_to_c(token));
+                        addr->next = 0;
+                        *al = addr;
+                        al = &addr->next;
+                } 
+                if(a->addr == nil || a->addr->next == nil){
+                        freealias(a);
+                        continue;
+                }
+                a->next = nil;
+                *l = a;
+                l = &a->next;
+        }
+        s_freeinstack(sp);
+
+out:
+        s_free(file);
+        s_free(line);
+        s_free(token);
+        return first;
+}
+
+Addr*
+newaddr(char *name)
+{
+        Addr *a;
+
+        a = emalloc(sizeof(*a));
+        a->next = nil;
+        a->v = estrdup(name);
+        if(a->v == nil)
+                sysfatal("%r");
+        return a;
+}
+
+//
+//  expand personal aliases since the names are meaningless in
+//  other contexts
+//
+Addr*
+_expand(Addr *old, int *changedp)
+{
+        Alias *al;
+        Addr *first, *next, **l, *a;
+
+        *changedp = 0;
+        first = nil;
+        l = &first;
+        for(;old != nil; old = next){
+                next = old->next;
+                for(al = aliases; al != nil; al = al->next){
+                        if(strcmp(al->addr->v, old->v) == 0){
+                                for(a = al->addr->next; a != nil; a = a->next){
+                                        *l = newaddr(a->v);
+                                        if(*l == nil)
+                                                sysfatal("%r");
+                                        l = &(*l)->next;
+                                        *changedp = 1;
+                                }
+                                break;
+                        }
+                }
+                if(al != nil){
+                        freeaddr(old);
+                        continue;
+                }
+                *l = old;
+                old->next = nil;
+                l = &(*l)->next;
+        }
+        return first;
+}
+
+Addr*
+rexpand(Addr *old)
+{
+        int i, changed;
+
+        changed = 0;
+        for(i=0; i<32; i++){
+                old = _expand(old, &changed);
+                if(changed == 0)
+                        break;
+        }
+        return old;
+}
+
+Addr*
+unique(Addr *first)
+{
+        Addr *a, **l, *x;
+
+        for(a = first; a != nil; a = a->next){
+                for(l = &a->next; *l != nil;){
+                        if(strcmp(a->v, (*l)->v) == 0){
+                                x = *l;
+                                *l = x->next;
+                                freeaddr(x);
+                        } else
+                                l = &(*l)->next;
+                }
+        }
+        return first;
+}
+
+Addr*
+expand(int ac, char **av)
+{
+        Addr *first, **l;
+        int i;
+
+        first = nil;
+
+        // make a list of the starting addresses
+        l = &first;
+        for(i = 0; i < ac; i++){
+                *l = newaddr(av[i]);
+                if(*l == nil)
+                        sysfatal("%r");
+                l = &(*l)->next;
+        }
+
+        // recurse till we don't change any more
+        return unique(rexpand(first));
+}
+
+Addr*
+concataddr(Addr *a, Addr *b)
+{
+        Addr *oa;
+
+        if(a == nil)
+                return b;
+
+        oa = a;
+        for(; a->next; a=a->next)
+                ;
+        a->next = b;
+        return oa;
+}
+
+void
+freeaddr(Addr *ap)
+{
+        free(ap->v);
+        free(ap);
+}
+
+void
+freeaddrs(Addr *ap)
+{
+        Addr *next;
+
+        for(; ap; ap=next) {
+                next = ap->next;
+                freeaddr(ap);
+        }
+}
+
+String*
+s_copyn(char *s, int n)
+{
+        return s_nappend(s_reset(nil), s, n);
+}
+
+// fetch the next token from an RFC822 address string
+// we assume the header is RFC822-conformant in that
+// we recognize escaping anywhere even though it is only
+// supposed to be in quoted-strings, domain-literals, and comments.
+//
+// i'd use yylex or yyparse here, but we need to preserve 
+// things like comments, which i think it tosses away.
+//
+// we're not strictly RFC822 compliant.  we misparse such nonsense as
+//
+//        To: gre @ (Grace) plan9 . (Emlin) bell-labs.com
+//
+// make sure there's no whitespace in your addresses and 
+// you'll be fine.
+//
+enum {
+        Twhite,
+        Tcomment,
+        Twords,
+        Tcomma,
+        Tleftangle,
+        Trightangle,
+        Terror,
+        Tend,
+};
+//char *ty82[] = {"white", "comment", "words", "comma", "<", ">", "err", "end"};
+#define ISWHITE(p) ((p)==' ' || (p)=='\t' || (p)=='\n' || (p)=='\r')
+int
+get822token(String **tok, char *p, char **pp)
+{
+        char *op;
+        int type;
+        int quoting;
+
+        op = p;
+        switch(*p){
+        case '\0':
+                *tok = nil;
+                *pp = nil;
+                return Tend;
+
+        case ' ':        // get whitespace
+        case '\t':
+        case '\n':
+        case '\r':
+                type = Twhite;
+                while(ISWHITE(*p))
+                        p++;
+                break;
+
+        case '(':        // get comment
+                type = Tcomment;
+                for(p++; *p && *p != ')'; p++)
+                        if(*p == '\\') {
+                                if(*(p+1) == '\0') {
+                                        *tok = nil;
+                                        return Terror;
+                                }
+                                p++;
+                        }
+
+                if(*p != ')') {
+                        *tok = nil;
+                        return Terror;
+                }
+                p++;
+                break;
+        case ',':
+                type = Tcomma;
+                p++;
+                break;
+        case '<':
+                type = Tleftangle;
+                p++;
+                break;
+        case '>':
+                type = Trightangle;
+                p++;
+                break;
+        default:        // bunch of letters, perhaps quoted strings tossed in
+                type = Twords;
+                quoting = 0;
+                for(; *p && (quoting || (!ISWHITE(*p) && *p != '>' && *p != '<' && *p != ',')); p++) {
+                        if(*p == '"') 
+                                quoting = !quoting;
+                        if(*p == '\\') {
+                                if(*(p+1) == '\0') {
+                                        *tok = nil;
+                                        return Terror;
+                                }
+                                p++;
+                        }
+                }
+                break;
+        }
+
+        if(pp)
+                *pp = p;
+        *tok = s_copyn(op, p-op);
+        return type;
+}        
+
+// expand local aliases in an RFC822 mail line
+// add list of expanded addresses to to.
+Addr*
+expandline(String **s, Addr *to)
+{
+        Addr *na, *nto, *ap;
+        char *p;
+        int tok, inangle, hadangle, nword;
+        String *os, *ns, *stok, *lastword, *sinceword;
+
+        os = s_copy(s_to_c(*s));
+        p = strchr(s_to_c(*s), ':');
+        assert(p != nil);
+        p++;
+
+        ns = s_copyn(s_to_c(*s), p-s_to_c(*s));
+        stok = nil;
+        nto = nil;
+        //
+        // the only valid mailbox namings are word
+        // and word* < addr >
+        // without comments this would be simple.
+        // we keep the following:
+        //        lastword - current guess at the address
+        //        sinceword - whitespace and comment seen since lastword
+        //
+        lastword = s_new();
+        sinceword = s_new();
+        inangle = 0;
+        nword = 0;
+        hadangle = 0;
+        for(;;) {
+                stok = nil;
+                switch(tok = get822token(&stok, p, &p)){
+                default:
+                        abort();
+                case Tcomma:
+                case Tend:
+                        if(inangle)
+                                goto Error;
+                        if(nword != 1)
+                                goto Error;
+                        na = rexpand(newaddr(s_to_c(lastword)));
+                        s_append(ns, na->v);
+                        s_append(ns, s_to_c(sinceword));
+                        for(ap=na->next; ap; ap=ap->next) {
+                                s_append(ns, ", ");
+                                s_append(ns, ap->v);
+                        }
+                        nto = concataddr(na, nto);
+                        if(tok == Tcomma){
+                                s_append(ns, ",");
+                                s_free(stok);
+                        }
+                        if(tok == Tend)
+                                goto Break2;
+                        inangle = 0;
+                        nword = 0;
+                        hadangle = 0;
+                        s_reset(sinceword);
+                        s_reset(lastword);
+                        break;
+                case Twhite:
+                case Tcomment:
+                        s_append(sinceword, s_to_c(stok));
+                        s_free(stok);
+                        break;
+                case Trightangle:
+                        if(!inangle)
+                                goto Error;
+                        inangle = 0;
+                        hadangle = 1;
+                        s_append(sinceword, s_to_c(stok));
+                        s_free(stok);
+                        break;
+                case Twords:
+                case Tleftangle:
+                        if(hadangle)
+                                goto Error;
+                        if(tok != Tleftangle && inangle && s_len(lastword))
+                                goto Error;
+                        if(tok == Tleftangle) {
+                                inangle = 1;
+                                nword = 1;
+                        }
+                        s_append(ns, s_to_c(lastword));
+                        s_append(ns, s_to_c(sinceword));
+                        s_reset(sinceword);
+                        if(tok == Tleftangle) {
+                                s_append(ns, "<");
+                                s_reset(lastword);
+                        } else {
+                                s_free(lastword);
+                                lastword = stok;
+                        }
+                        if(!inangle)
+                                nword++;
+                        break;
+                case Terror:        // give up, use old string, addrs
+                Error:
+                        ns = os;
+                        os = nil;
+                        freeaddrs(nto);
+                        nto = nil;
+                        werrstr("rfc822 syntax error");
+                        rfc822syntaxerror = 1;
+                        goto Break2;                        
+                }
+        }
+Break2:
+        s_free(*s);
+        s_free(os);
+        *s = ns;
+        nto = concataddr(nto, to);
+        return nto;
+}
+
+void
+Bdrain(Biobuf *b)
+{
+        char buf[8192];
+
+        while(Bread(b, buf, sizeof buf) > 0)
+                ;
+}
+
+void
+readmimetypes(void)
+{
+        Biobuf *b;
+        char *p;
+        char *f[6];
+        char type[256];
+        static int alloced, inuse;
+
+        if(mimetypes == 0){
+                alloced = 256;
+                mimetypes = emalloc(alloced*sizeof(Ctype));
+                mimetypes[0].ext = "";
+        }
+
+        b = Bopen(unsharp("#9/sys/lib/mimetype"), OREAD);
+        if(b == nil)
+                return;
+        for(;;){
+                p = Brdline(b, '\n');
+                if(p == nil)
+                        break;
+                p[Blinelen(b)-1] = 0;
+                if(tokenize(p, f, 6) < 4)
+                        continue;
+                if(strcmp(f[0], "-") == 0 || strcmp(f[1], "-") == 0 || strcmp(f[2], "-") == 0)
+                        continue;
+                if(inuse + 1 >= alloced){
+                        alloced += 256;
+                        mimetypes = erealloc(mimetypes, alloced*sizeof(Ctype));
+                }
+                snprint(type, sizeof(type), "%s/%s", f[1], f[2]);
+                mimetypes[inuse].type = estrdup(type);
+                mimetypes[inuse].ext = estrdup(f[0]+1);
+                mimetypes[inuse].display = !strcmp(type, "text/plain");
+                inuse++;
+
+                // always make sure there's a terminator
+                mimetypes[inuse].ext = 0;
+        }
+        Bterm(b);
+}
+
+char*
+estrdup(char *x)
+{
+        x = strdup(x);
+        if(x == nil)
+                fatal("memory");
+        return x;
+}
+
+void*
+emalloc(int n)
+{
+        void *x;
+
+        x = malloc(n);
+        if(x == nil)
+                fatal("%r");
+        return x;
+}
+
+void*
+erealloc(void *x, int n)
+{
+        x = realloc(x, n);
+        if(x == nil)
+                fatal("%r");
+        return x;
+}
+
+//
+// Formatter for %"
+// Use double quotes to protect white space, frogs, \ and "
+//
+enum
+{
+        Qok = 0,
+        Qquote,
+        Qbackslash,
+};
+
+static int
+needtoquote(Rune r)
+{
+        if(r >= Runeself)
+                return Qquote;
+        if(r <= ' ')
+                return Qquote;
+        if(r=='\\' || r=='"')
+                return Qbackslash;
+        return Qok;
+}
+
+int
+doublequote(Fmt *f)
+{
+        char *s, *t;
+        int w, quotes;
+        Rune r;
+
+        s = va_arg(f->args, char*);
+        if(s == nil || *s == '\0')
+                return fmtstrcpy(f, "\"\"");
+
+        quotes = 0;
+        for(t=s; *t; t+=w){
+                w = chartorune(&r, t);
+                quotes |= needtoquote(r);
+        }
+        if(quotes == 0)
+                return fmtstrcpy(f, s);
+
+        fmtrune(f, '"');
+        for(t=s; *t; t+=w){
+                w = chartorune(&r, t);
+                if(needtoquote(r) == Qbackslash)
+                        fmtrune(f, '\\');
+                fmtrune(f, r);
+        }
+        return fmtrune(f, '"');
+}
diff --git a/src/cmd/upas/marshal/mkfile b/src/cmd/upas/marshal/mkfile
t@@ -0,0 +1,20 @@
+<$PLAN9/src/mkhdr
+
+TARG=marshal
+
+LIB=../common/libcommon.a\
+
+HFILES=        ../common/common.h\
+
+OFILES= marshal.$O
+
+BIN=$PLAN9/bin/upas
+
+UPDATE=\
+        mkfile\
+        $HFILES\
+        ${OFILES:%.$O=%.c}\
+        
+<$PLAN9/src/mkone
+CFLAGS=$CFLAGS -I../common
+
diff --git a/src/cmd/upas/misc/gone.fishing b/src/cmd/upas/misc/gone.fishing
t@@ -0,0 +1,9 @@
+#!/bin/sh
+PATH=/bin:/usr/bin
+message=${1-/usr/lib/upas/gone.msg}
+return=`sed '2,$s/^From[         ]/>&/'|tee -a $HOME/gone.mail|sed -n '1s/^From[         ]\([^         ]*\)[         ].*$/\1/p'`
+echo '' >>$HOME/gone.mail
+grep "^$return" $HOME/gone.addrs >/dev/null 2>/dev/null || {
+        echo $return >>$HOME/gone.addrs
+        mail $return < $message
+}
diff --git a/src/cmd/upas/misc/gone.msg b/src/cmd/upas/misc/gone.msg
t@@ -0,0 +1,4 @@
+This is a recorded message.  I am currently out of contact with my
+computer system.  Your message to me has been saved and will be
+read upon my return.  This is the last time you will receive this
+message during my absence.  Thank you.
diff --git a/src/cmd/upas/misc/mail.c b/src/cmd/upas/misc/mail.c
t@@ -0,0 +1,51 @@
+/*
+ * #!/bin/sh
+ * case $1 in
+ * -n)
+ *         exit 0 ;;
+ * -m*|-f*|-r*|-p*|-e*|"")
+ *         exec /usr/lib/upas/edmail $*
+ *         exit $? ;;
+ * *)
+ *         exec /usr/lib/upas/send $*
+ *         exit $? ;;
+ * esac
+ */
+
+
+extern *UPASROOT;
+
+#define        EDMAIL        "edmail"
+#define        SEND        "send"
+
+main (argc, argv)
+        int argc;
+        char **argv;
+{
+        char *progname = SEND;
+        char realprog[500];
+
+        if (argc > 1) {
+                if (argv[1][0] == '-') {
+                        switch (argv[1][1]) {
+                        case 'n':
+                                exit (0);
+
+                        case 'm':
+                        case 'f':
+                        case 'r':
+                        case 'p':
+                        case 'e':
+                        case '\0':
+                                progname = EDMAIL;
+                        }
+                }
+        } else
+                progname = EDMAIL;
+
+        sprint(realprog, "%s/%s", UPASROOT, progname);
+        execv (realprog, argv);
+        perror (realprog);
+        exit (1);
+}
+
diff --git a/src/cmd/upas/misc/mail.rc b/src/cmd/upas/misc/mail.rc
t@@ -0,0 +1,12 @@
+#!/bin/rc
+switch($#*){
+case 0
+        exec upas/nedmail
+}
+
+switch($1){
+case -f* -r* -c* -m*
+        exec upas/nedmail $*
+case *
+        exec upas/marshal $*
+}
diff --git a/src/cmd/upas/misc/mail.sh b/src/cmd/upas/misc/mail.sh
t@@ -0,0 +1,12 @@
+#!/bin/sh
+case $1 in
+-n)
+        exec LIBDIR/notify
+        exit $? ;;
+-m*|-f*|-r*|-p*|-e*|"")
+        exec LIBDIR/edmail $*
+        exit $? ;;
+*)
+        exec LIBDIR/send $*
+        exit $? ;;
+esac
diff --git a/src/cmd/upas/misc/makefile b/src/cmd/upas/misc/makefile
t@@ -0,0 +1,44 @@
+LIB=/usr/lib/upas
+CFLAGS=${UNIX} -g -I. -I../libc -I../common -I/usr/include -I/usr/include/sys
+LFLAGS=-g
+HOSTNAME=cat /etc/whoami
+
+.c.o: ; $(CC) -c $(CFLAGS) $*.c
+all: mail
+
+sedfile:
+        echo 's+LIBDIR+$(LIB)+g' >sed.file
+        echo 's+HOSTNAME+$(HOSTNAME)+g' >>sed.file
+
+install: sedfile install.fish install.mail.sh
+
+install.fish:
+        cp gone.msg $(LIB)
+        sed -f sed.file gone.fishing >$(LIB)/gone.fishing
+        -chmod 775 $(LIB)/gone.fishing
+        -chown bin $(LIB)/gone.fishing $(LIB)/gone.msg
+
+install.mail.sh:
+        sed -f sed.file mail.sh >/bin/mail
+        -chown bin /bin/mail
+        -chmod 775 /bin/mail
+
+install.notify: notify
+        cp notify $(LIB)/notify
+        -chmod 775 $(LIB)/notify
+        -chown bin $(LIB)/notify
+
+install.mail: mail
+        cp mail /bin
+        strip /bin/mail
+
+notify: notify.o
+        cc $(LFLAGS) notify.o -o notify
+
+mail: mail.o ../config/config.o
+        cc $(LFLAGS) mail.o ../config/config.o -o mail
+
+clean:
+        -rm -f *.[oOa] core a.out *.sL notify
+        -rm -f sed.file mail
+
diff --git a/src/cmd/upas/misc/mkfile b/src/cmd/upas/misc/mkfile
t@@ -0,0 +1,39 @@
+
+RCFILES=mail.rc\
+
+all:Q:
+        ;
+
+installall:Q:        install
+        ;
+
+install:V:
+        cp mail.rc /rc/bin/mail
+
+safeinstall:V:
+        cp mail.rc /rc/bin/mail
+
+safeinstallall:V:
+        cp mail.rc /rc/bin/mail
+
+clean:Q:
+        ;
+nuke:V:
+        rm /rc/bin/mail
+
+UPDATE=\
+        gone.fishing\
+        gone.msg\
+        mail.c\
+        mail.rc\
+        mail.sh\
+        makefile\
+        mkfile\
+        namefiles\
+        omail.rc\
+        qmail\
+        remotemail\
+        rewrite\
+
+update:V:
+        update $UPDATEFLAGS $UPDATE
diff --git a/src/cmd/upas/misc/namefiles b/src/cmd/upas/misc/namefiles
t@@ -0,0 +1,2 @@
+names.local
+names.global
diff --git a/src/cmd/upas/misc/omail.rc b/src/cmd/upas/misc/omail.rc
t@@ -0,0 +1,14 @@
+#!/bin/rc
+switch($#*){
+case 0
+        exec upas/edmail -m
+}
+
+switch($1){
+case -F* -m* -f* -r* -p* -e* -c* -D*
+        exec upas/edmail -m $*
+case '-#'* -a*
+        exec upas/sendmail $*
+case *
+        exec upas/sendmail $*
+}
diff --git a/src/cmd/upas/misc/qmail b/src/cmd/upas/misc/qmail
t@@ -0,0 +1,6 @@
+#!/bin/rc
+sender=$1
+shift
+addr=$1
+shift
+qer /mail/queue mail $sender $addr $* && runq /mail/queue /mail/lib/remotemail
diff --git a/src/cmd/upas/misc/remotemail b/src/cmd/upas/misc/remotemail
t@@ -0,0 +1,7 @@
+#!/bin/rc
+shift
+sender=$1
+shift
+addr=$1
+shift
+/bin/upas/smtp -g research.research.bell-labs.com $addr $sender $*
diff --git a/src/cmd/upas/misc/rewrite b/src/cmd/upas/misc/rewrite
t@@ -0,0 +1,20 @@
+# case conversion for postmaster
+pOsTmAsTeR        alias                postmaster
+
+# local mail
+[^!@]+                translate        "/bin/upas/aliasmail '&'"
+local!(.*)        >>                /mail/box/\1/mbox
+\l!(.*)                alias                \1
+(helix|helix.bell-labs.com)!(.*)        alias                \2
+
+# we can be just as complicated as BSD sendmail...
+# convert source domain address to a chain a@b@c@d...
+@([^@!,]*):([^!@]*)@([^!]*)        alias        \2@\3@\1
+@([^@!]*),([^!@,]*):([^!@]*)@([^!]*)        alias        @\1:\3@\4@\2
+
+# convert a chain a@b@c@d... to ...d!c!b!a
+([^@]+)@([^@]+)@(.+)        alias        \2!\1@\3
+([^@]+)@([^@]+)                alias        \2!\1
+
+# /mail/lib/remotemail will take care of gating to systems we don't know
+([^!]*)!(.*)                 |                 "/mail/lib/qmail '\s' 'net!\1'" "'\2'"
diff --git a/src/cmd/upas/ml/common.c b/src/cmd/upas/ml/common.c
t@@ -0,0 +1,197 @@
+#include "common.h"
+#include "dat.h"
+
+String*
+getaddr(Node *p)
+{
+        for(; p; p = p->next){
+                if(p->s && p->addr)
+                        return p->s;
+        }
+        return nil;
+}
+
+/* send messae adding our own reply-to and precedence */
+void
+getaddrs(void)
+{
+        Field *f;
+
+        for(f = firstfield; f; f = f->next){
+                if(f->node->c == FROM && from == nil)
+                        from = getaddr(f->node);
+                if(f->node->c == SENDER && sender == nil)
+                        sender = getaddr(f->node);
+        }
+}
+
+/* write address file, should be append only */
+void
+writeaddr(char *file, char *addr, int rem, char *listname)
+{
+        int fd;
+        Dir nd;
+
+        fd = open(file, OWRITE);
+        if(fd < 0){
+                fd = create(file, OWRITE, DMAPPEND|0666);
+                if(fd < 0)
+                        sysfatal("creating address list %s: %r", file);
+                nulldir(&nd);
+                nd.mode = DMAPPEND|0666;
+                dirwstat(file, &nd);
+        } else
+                seek(fd, 0, 2);
+        if(rem)
+                fprint(fd, "!%s\n", addr);
+        else
+                fprint(fd, "%s\n", addr);
+        close(fd);
+
+        if(*addr != '#')
+                sendnotification(addr, listname, rem);
+}
+
+void
+remaddr(char *addr)
+{
+        Addr **l;
+        Addr *a;
+
+        for(l = &al; *l; l = &(*l)->next){
+                a = *l;
+                if(strcmp(addr, a->addr) == 0){
+                        (*l) = a->next;
+                        free(a);
+                        na--;
+                        break;
+                }
+        }
+}
+
+int
+addaddr(char *addr)
+{
+        Addr **l;
+        Addr *a;
+
+        for(l = &al; *l; l = &(*l)->next){
+                if(strcmp(addr, (*l)->addr) == 0)
+                        return 0;
+        }
+        na++;
+        *l = a = malloc(sizeof(*a)+strlen(addr)+1);
+        if(a == nil)
+                sysfatal("allocating: %r");
+        a->addr = (char*)&a[1];
+        strcpy(a->addr, addr);
+        a->next = nil;
+        *l = a;
+        return 1;
+}
+
+/* read address file */
+void
+readaddrs(char *file)
+{
+        Biobuf *b;
+        char *p;
+
+        b = Bopen(file, OREAD);
+        if(b == nil)
+                return;
+
+        while((p = Brdline(b, '\n')) != nil){
+                p[Blinelen(b)-1] = 0;
+                if(*p == '#')
+                        continue;
+                if(*p == '!')
+                        remaddr(p+1);
+                else
+                        addaddr(p);
+        }
+        Bterm(b);
+}
+
+/* start a mailer sending to all the receivers */
+int
+startmailer(char *name)
+{
+        int pfd[2];
+        char **av;
+        int ac;
+        Addr *a;
+
+        putenv("upasname", "/dev/null");
+        if(pipe(pfd) < 0)
+                sysfatal("creating pipe: %r");
+        switch(fork()){
+        case -1:
+                sysfatal("starting mailer: %r");
+        case 0:
+                close(pfd[1]);
+                break;
+        default:
+                close(pfd[0]);
+                return pfd[1];
+        }
+
+        dup(pfd[0], 0);
+        close(pfd[0]);
+
+        av = malloc(sizeof(char*)*(na+2));
+        if(av == nil)
+                sysfatal("starting mailer: %r");
+        ac = 0;
+        av[ac++] = name;
+        for(a = al; a != nil; a = a->next)
+                av[ac++] = a->addr;
+        av[ac] = 0;
+        exec("/bin/upas/send", av);
+        sysfatal("execing mailer: %r");
+
+        /* not reached */
+        return -1;
+}
+
+void
+sendnotification(char *addr, char *listname, int rem)
+{
+        int pfd[2];
+        Waitmsg *w;
+
+        putenv("upasname", "/dev/null");
+        if(pipe(pfd) < 0)
+                sysfatal("creating pipe: %r");
+        switch(fork()){
+        case -1:
+                sysfatal("starting mailer: %r");
+        case 0:
+                close(pfd[1]);
+                dup(pfd[0], 0);
+                close(pfd[0]);
+                execl("/bin/upas/send", "mlnotify", addr, nil);
+                sysfatal("execing mailer: %r");
+                break;
+        default:
+                close(pfd[0]);
+                fprint(pfd[1], "From: %s-owner\n\n", listname);
+                if(rem)
+                        fprint(pfd[1], "You have removed from the %s mailing list\n", listname);
+                else{
+                        fprint(pfd[1], "You have been added to the %s mailing list\n", listname);
+                        fprint(pfd[1], "To be removed, send an email to %s-owner containing\n",
+                                listname);
+                        fprint(pfd[1], "the word 'remove' in the subject or body.\n");
+                }
+                close(pfd[1]);
+        
+                /* wait for mailer to end */
+                while(w = wait()){
+                        if(w->msg != nil && w->msg[0])
+                                sysfatal("%s", w->msg);
+                        free(w);
+                }
+                break;
+        }
+}
diff --git a/src/cmd/upas/ml/dat.h b/src/cmd/upas/ml/dat.h
t@@ -0,0 +1,25 @@
+
+#include "../smtp/smtp.h"
+#include "../smtp/y.tab.h"
+
+typedef struct Addr Addr;
+struct Addr
+{
+        char *addr;
+        Addr *next;
+};
+
+String *from;
+String *sender;
+Field *firstfield;
+int na;
+Addr *al;
+
+extern String*        getaddr(Node *p);
+extern void        getaddrs(void);
+extern void        writeaddr(char *file, char *addr, int, char *);
+extern void        remaddr(char *addr);
+extern int        addaddr(char *addr);
+extern void        readaddrs(char *file);
+extern int        startmailer(char *name);
+extern void        sendnotification(char *addr, char *listname, int rem);
diff --git a/src/cmd/upas/ml/mkfile b/src/cmd/upas/ml/mkfile
t@@ -0,0 +1,40 @@
+
diff --git a/src/cmd/upas/ml/ml.c b/src/cmd/upas/ml/ml.c
t@@ -0,0 +1,167 @@
+#include "common.h"
+#include "dat.h"
+
+Biobuf in;
+
+Addr *al;
+int na;
+String *from;
+String *sender;
+
+void printmsg(int fd, String *msg, char *replyto, char *listname);
+void appendtoarchive(char* listname, String *firstline, String *msg);
+void printsubject(int fd, Field *f, char *listname);
+
+void
+usage(void)
+{
+        fprint(2, "usage: %s address-list-file listname\n", argv0);
+        exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+        String *msg;
+        String *firstline;
+        char *listname, *alfile;
+        Waitmsg *w;
+        int fd;
+        char *replytoname = nil;
+
+        ARGBEGIN{
+        case 'r':
+                replytoname = ARGF();
+                break;
+        }ARGEND;
+
+        rfork(RFENVG|RFREND);
+
+        if(argc < 2)
+                usage();
+        alfile = argv[0];
+        listname = argv[1];
+        if(replytoname == nil)
+                replytoname = listname;
+
+        readaddrs(alfile);
+
+        if(Binit(&in, 0, OREAD) < 0)
+                sysfatal("opening input: %r");
+
+        msg = s_new();
+        firstline = s_new();
+
+        /* discard the 'From ' line */
+        if(s_read_line(&in, firstline) == nil)
+                sysfatal("reading input: %r");
+
+        /* read up to the first 128k of the message.  more is redculous. 
+             Not if word documents are distributed.  Upped it to 2MB (pb) */
+        if(s_read(&in, msg, 2*1024*1024) <= 0)
+                sysfatal("reading input: %r");
+
+        /* parse the header */
+        yyinit(s_to_c(msg), s_len(msg));
+        yyparse();
+
+        /* get the sender */
+        getaddrs();
+        if(from == nil)
+                from = sender;
+        if(from == nil)
+                sysfatal("message must contain From: or Sender:");
+        if(strcmp(listname, s_to_c(from)) == 0)
+                sysfatal("can't remail messages from myself");
+        addaddr(s_to_c(from));
+
+        /* start the mailer up and return a pipe to it */
+        fd = startmailer(listname);
+
+        /* send message adding our own reply-to and precedence */
+        printmsg(fd, msg, replytoname, listname);
+        close(fd);
+
+        /* wait for mailer to end */
+        while(w = wait()){
+                if(w->msg != nil && w->msg[0])
+                        sysfatal("%s", w->msg);
+                free(w);
+        }
+
+        /* if the mailbox exits, cat the mail to the end of it */
+        appendtoarchive(listname, firstline, msg);
+        exits(0);
+}
+
+/* send message filtering Reply-to out of messages */
+void
+printmsg(int fd, String *msg, char *replyto, char *listname)
+{
+        Field *f, *subject;
+        Node *p;
+        char *cp, *ocp;
+
+        subject = nil;
+        cp = s_to_c(msg);
+        for(f = firstfield; f; f = f->next){
+                ocp = cp;
+                for(p = f->node; p; p = p->next)
+                        cp = p->end+1;
+                if(f->node->c == REPLY_TO)
+                        continue;
+                if(f->node->c == PRECEDENCE)
+                        continue;
+                if(f->node->c == SUBJECT){
+                        subject = f;
+                        continue;
+                }
+                write(fd, ocp, cp-ocp);
+        }
+        printsubject(fd, subject, listname);
+        fprint(fd, "Reply-To: %s\nPrecedence: bulk\n", replyto);
+        write(fd, cp, s_len(msg) - (cp - s_to_c(msg)));
+}
+
+/* if the mailbox exits, cat the mail to the end of it */
+void
+appendtoarchive(char* listname, String *firstline, String *msg)
+{
+        String *mbox;
+        int fd;
+
+        mbox = s_new();
+        mboxpath("mbox", listname, mbox, 0);
+        if(access(s_to_c(mbox), 0) < 0)
+                return;
+        fd = open(s_to_c(mbox), OWRITE);
+        if(fd < 0)
+                return;
+        s_append(msg, "\n");
+        write(fd, s_to_c(firstline), s_len(firstline));
+        write(fd, s_to_c(msg), s_len(msg));
+}
+
+/* add the listname to the subject */
+void
+printsubject(int fd, Field *f, char *listname)
+{
+        char *s, *e;
+        Node *p;
+        char *ln;
+
+        if(f == nil || f->node == nil){
+                fprint(fd, "Subject: [%s]\n", listname);
+                return;
+        }
+        s = e = f->node->end + 1;
+        for(p = f->node; p; p = p->next)
+                e = p->end;
+        *e = 0;
+        ln = smprint("[%s]", listname);
+        if(ln != nil && strstr(s, ln) == nil)
+                fprint(fd, "Subject: %s%s\n", ln, s);
+        else
+                fprint(fd, "Subject:%s\n", s);
+        free(ln);
+}
diff --git a/src/cmd/upas/ml/mlmgr.c b/src/cmd/upas/ml/mlmgr.c
t@@ -0,0 +1,110 @@
+#include "common.h"
+#include "dat.h"
+
+int cflag;
+int aflag;
+int rflag;
+
+int createpipeto(char *alfile, char *user, char *listname, int owner);
+
+void
+usage(void)
+{
+        fprint(2, "usage:\t%s -c listname\n", argv0);
+        fprint(2, "\t%s -[ar] listname addr\n", argv0);
+        exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+        char *listname, *addr;
+        String *owner, *alfile;
+
+        rfork(RFENVG|RFREND);
+
+        ARGBEGIN{
+        case 'c':
+                cflag = 1;
+                break;
+        case 'r':
+                rflag = 1;
+                break;
+        case 'a':
+                aflag = 1;
+                break;
+        }ARGEND;
+
+        if(aflag + rflag + cflag > 1){
+                fprint(2, "%s: -a, -r, and -c are mutually exclusive\n", argv0);
+                exits("usage");
+        }
+
+        if(argc < 1)
+                usage();
+
+        listname = argv[0];
+        alfile = s_new();
+        mboxpath("address-list", listname, alfile, 0);
+
+        if(cflag){
+                owner = s_copy(listname);
+                s_append(owner, "-owner");
+                if(creatembox(listname, nil) < 0)
+                        sysfatal("creating %s's mbox: %r", listname);
+                if(creatembox(s_to_c(owner), nil) < 0)
+                        sysfatal("creating %s's mbox: %r", s_to_c(owner));
+                if(createpipeto(s_to_c(alfile), listname, listname, 0) < 0)
+                        sysfatal("creating %s's pipeto: %r", s_to_c(owner));
+                if(createpipeto(s_to_c(alfile), s_to_c(owner), listname, 1) < 0)
+                        sysfatal("creating %s's pipeto: %r", s_to_c(owner));
+                writeaddr(s_to_c(alfile), "# mlmgr c flag", 0, listname);
+        } else if(rflag){
+                if(argc != 2)
+                        usage();
+                addr = argv[1];
+                writeaddr(s_to_c(alfile), "# mlmgr r flag", 0, listname);
+                writeaddr(s_to_c(alfile), addr, 1, listname);
+        } else if(aflag){
+                if(argc != 2)
+                        usage();
+                addr = argv[1];
+                writeaddr(s_to_c(alfile), "# mlmgr a flag", 0, listname);
+                writeaddr(s_to_c(alfile), addr, 0, listname);
+        } else
+                usage();
+        exits(0);
+}
+
+int
+createpipeto(char *alfile, char *user, char *listname, int owner)
+{
+        String *f;
+        int fd;
+        Dir *d;
+
+        f = s_new();
+        mboxpath("pipeto", user, f, 0);
+        fprint(2, "creating new pipeto: %s\n", s_to_c(f));
+        fd = create(s_to_c(f), OWRITE, 0775);
+        if(fd < 0)
+                return -1;
+        d = dirfstat(fd);
+        if(d == nil){
+                fprint(fd, "Couldn't stat %s: %r\n", s_to_c(f));
+                return -1;
+        }
+        d->mode |= 0775;
+        if(dirfwstat(fd, d) < 0)
+                fprint(fd, "Couldn't wstat %s: %r\n", s_to_c(f));
+        free(d);
+
+        fprint(fd, "#!/bin/rc\n");
+        if(owner)
+                fprint(fd, "/bin/upas/mlowner %s %s\n", alfile, listname);
+        else
+                fprint(fd, "/bin/upas/ml %s %s\n", alfile, user);
+        close(fd);
+
+        return 0;
+}
diff --git a/src/cmd/upas/ml/mlowner.c b/src/cmd/upas/ml/mlowner.c
t@@ -0,0 +1,64 @@
+#include "common.h"
+#include "dat.h"
+
+Biobuf in;
+
+String *from;
+String *sender;
+
+
+void
+usage(void)
+{
+        fprint(2, "usage: %s address-list-file listname\n", argv0);
+        exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+        String *msg;
+        char *alfile;
+        char *listname;
+
+        ARGBEGIN{
+        }ARGEND;
+
+        rfork(RFENVG|RFREND);
+
+        if(argc < 2)
+                usage();
+        alfile = argv[0];
+        listname = argv[1];
+
+        if(Binit(&in, 0, OREAD) < 0)
+                sysfatal("opening input: %r");
+
+        msg = s_new();
+
+        /* discard the 'From ' line */
+        if(s_read_line(&in, msg) == nil)
+                sysfatal("reading input: %r");
+
+        /* read up to the first 128k of the message.  more is redculous */
+        if(s_read(&in, s_restart(msg), 128*1024) <= 0)
+                sysfatal("reading input: %r");
+
+        /* parse the header */
+        yyinit(s_to_c(msg), s_len(msg));
+        yyparse();
+
+        /* get the sender */
+        getaddrs();
+        if(from == nil)
+                from = sender;
+        if(from == nil)
+                sysfatal("message must contain From: or Sender:");
+
+        if(strstr(s_to_c(msg), "remove")||strstr(s_to_c(msg), "unsubscribe"))
+                writeaddr(alfile, s_to_c(from), 1, listname);
+        else if(strstr(s_to_c(msg), "subscribe"))
+                writeaddr(alfile, s_to_c(from), 0, listname);
+
+        exits(0);
+}
diff --git a/src/cmd/upas/ned/mkfile b/src/cmd/upas/ned/mkfile
t@@ -0,0 +1,20 @@
+<$PLAN9/src/mkhdr
+
+TARG=nedmail
+
+LIB=../common/libcommon.a\
+
+HFILES=        ../common/common.h\
+
+OFILES=nedmail.$O
+
+BIN=$PLAN9/bin/upas
+
+UPDATE=\
+        mkfile\
+        ${OFILES:%.$O=%.c}\
+        $HFILES\
+
+<$PLAN9/src/mkone
+CFLAGS=$CFLAGS -I../common
+
diff --git a/src/cmd/upas/ned/nedmail.c b/src/cmd/upas/ned/nedmail.c
t@@ -0,0 +1,2586 @@
+#include "common.h"
+#include 
+#include 
+#include <9pclient.h>
+#include 
+
+typedef struct Message Message;
+typedef struct Ctype Ctype;
+typedef struct Cmd Cmd;
+
+char        root[Pathlen];
+char        mbname[Elemlen];
+int        rootlen;
+int        didopen;
+char        *user;
+char        wd[2048];
+String        *mbpath;
+int        natural;
+int        doflush;
+
+int interrupted;
+
+struct Message {
+        Message        *next;
+        Message        *prev;
+        Message        *cmd;
+        Message        *child;
+        Message        *parent;
+        String        *path;
+        int        id;
+        int        len;
+        int        fileno;        // number of directory
+        String        *info;
+        char        *from;
+        char        *to;
+        char        *cc;
+        char        *replyto;
+        char        *date;
+        char        *subject;
+        char        *type;
+        char        *disposition;
+        char        *filename;
+        char        deleted;
+        char        stored;
+};
+
+Message top;
+
+struct Ctype {
+        char        *type;
+        char         *ext;
+        int        display;
+        char        *plumbdest;
+        Ctype        *next;
+};
+
+Ctype ctype[] = {
+        { "text/plain",                        "txt",        1,        0        },
+        { "text/html",                        "htm",        1,        0        },
+        { "text/html",                        "html",        1,        0        },
+        { "text/tab-separated-values",        "tsv",        1,        0        },
+        { "text/richtext",                "rtx",        1,        0        },
+        { "text/rtf",                        "rtf",        1,        0        },
+        { "text",                        "txt",        1,        0        },
+        { "message/rfc822",                "msg",        0,        0        },
+        { "image/bmp",                        "bmp",        0,        "image"        },
+        { "image/jpeg",                        "jpg",        0,        "image"        },
+        { "image/gif",                        "gif",        0,        "image"        },
+        { "application/pdf",                "pdf",        0,        "postscript"        },
+        { "application/postscript",        "ps",        0,        "postscript"        },
+        { "application/",                0,        0,        0        },
+        { "image/",                        0,        0,        0        },
+        { "multipart/",                        "mul",        0,        0        },
+
+};
+
+Message*        acmd(Cmd*, Message*);
+Message*        bcmd(Cmd*, Message*);
+Message*        dcmd(Cmd*, Message*);
+Message*        eqcmd(Cmd*, Message*);
+Message*        hcmd(Cmd*, Message*);
+Message*        Hcmd(Cmd*, Message*);
+Message*        helpcmd(Cmd*, Message*);
+Message*        icmd(Cmd*, Message*);
+Message*        pcmd(Cmd*, Message*);
+Message*        qcmd(Cmd*, Message*);
+Message*        rcmd(Cmd*, Message*);
+Message*        scmd(Cmd*, Message*);
+Message*        ucmd(Cmd*, Message*);
+Message*        wcmd(Cmd*, Message*);
+Message*        xcmd(Cmd*, Message*);
+Message*        ycmd(Cmd*, Message*);
+Message*        pipecmd(Cmd*, Message*);
+Message*        rpipecmd(Cmd*, Message*);
+Message*        bangcmd(Cmd*, Message*);
+Message*        Pcmd(Cmd*, Message*);
+Message*        mcmd(Cmd*, Message*);
+Message*        fcmd(Cmd*, Message*);
+Message*        quotecmd(Cmd*, Message*);
+
+struct {
+        char                *cmd;
+        int                args;
+        Message*        (*f)(Cmd*, Message*);
+        char                *help;
+} cmdtab[] = {
+        { "a",        1,        acmd,        "a        reply to sender and recipients" },
+        { "A",        1,        acmd,        "A        reply to sender and recipients with copy" },
+        { "b",        0,        bcmd,        "b        print the next 10 headers" },
+        { "d",        0,        dcmd,        "d        mark for deletion" },
+        { "f",        0,        fcmd,        "f        file message by from address" },
+        { "h",        0,        hcmd,        "h        print elided message summary (,h for all)" },
+        { "help", 0,        helpcmd, "help     print this info" },
+        { "H",        0,        Hcmd,        "H        print message's MIME structure " },
+        { "i",        0,        icmd,        "i        incorporate new mail" },
+        { "m",        1,        mcmd,        "m addr   forward mail" },
+        { "M",        1,        mcmd,        "M addr   forward mail with message" },
+        { "p",        0,        pcmd,        "p        print the processed message" },
+        { "P",        0,        Pcmd,        "P        print the raw message" },
+        { "\"",        0,        quotecmd, "\"        print a quoted version of msg" },
+        { "q",        0,        qcmd,        "q        exit and remove all deleted mail" },
+        { "r",        1,        rcmd,        "r [addr] reply to sender plus any addrs specified" },
+        { "rf",        1,        rcmd,        "rf [addr]file message and reply" },
+        { "R",        1,        rcmd,        "R [addr] reply including copy of message" },
+        { "Rf",        1,        rcmd,        "Rf [addr]file message and reply with copy" },
+        { "s",        1,        scmd,        "s file   append raw message to file" },
+        { "u",        0,        ucmd,        "u        remove deletion mark" },
+        { "w",        1,        wcmd,        "w file   store message contents as file" },
+        { "x",        0,        xcmd,        "x        exit without flushing deleted messages" },
+        { "y",        0,        ycmd,        "y        synchronize with mail box" },
+        { "=",        1,        eqcmd,        "=        print current message number" },
+        { "|",        1,        pipecmd, "|cmd     pipe message body to a command" },
+        { "||",        1,        rpipecmd, "||cmd     pipe raw message to a command" },
+        { "!",        1,        bangcmd, "!cmd     run a command" },
+        { nil,        0,        nil,         nil },
+};
+
+enum
+{
+        NARG=        32,
+};
+
+struct Cmd {
+        Message        *msgs;
+        Message        *(*f)(Cmd*, Message*);
+        int        an;
+        char        *av[NARG];
+        int        delete;
+};
+
+Biobuf out;
+int startedfs;
+int reverse;
+int longestfrom = 12;
+
+String*                file2string(String*, char*);
+int                dir2message(Message*, int);
+int                filelen(String*, char*);
+String*                extendpath(String*, char*);
+void                snprintheader(char*, int, Message*);
+void                cracktime(char*, char*, int);
+int                cistrncmp(char*, char*, int);
+int                cistrcmp(char*, char*);
+Reprog*                parsesearch(char**);
+char*                parseaddr(char**, Message*, Message*, Message*, Message**);
+char*                parsecmd(char*, Cmd*, Message*, Message*);
+char*                readline(char*, char*, int);
+void                messagecount(Message*);
+void                system9(char*, char**, int);
+void                mkid(String*, Message*);
+int                switchmb(char*, char*);
+void                closemb(void);
+int                lineize(char*, char**, int);
+int                rawsearch(Message*, Reprog*);
+Message*        dosingleton(Message*, char*);
+String*                rooted(String*);
+int                plumb(Message*, Ctype*);
+String*                addrecolon(char*);
+void                exitfs(char*);
+Message*        flushdeleted(Message*);
+
+CFsys *upasfs;
+
+void
+usage(void)
+{
+        fprint(2, "usage: %s [-nr] [-f mailfile] [-s mailfile]\n", argv0);
+        fprint(2, "       %s -c dir\n", argv0);
+        threadexits("usage");
+}
+
+void
+catchnote(void* dummy, char *note)
+{
+        if(strstr(note, "interrupt") != nil){
+                interrupted = 1;
+                noted(NCONT);
+        }
+        noted(NDFLT);
+}
+
+char *
+plural(int n)
+{
+        if (n == 1)
+                return "";
+
+        return "s";                
+}
+
+void
+threadmain(int argc, char **argv)
+{
+        Message *cur, *m, *x;
+        char cmdline[4*1024];
+        Cmd cmd;
+        Ctype *cp;
+        char *err;
+        int n, cflag;
+        char *av[4];
+        String *prompt;
+        char *file, *singleton;
+        char *fscmd;
+
+        Binit(&out, 1, OWRITE);
+
+        file = nil;
+        singleton = nil;
+        reverse = 1;
+        cflag = 0;
+        ARGBEGIN {
+        case 'c':
+                cflag = 1;
+                break;
+        case 'f':
+                file = EARGF(usage());
+                break;
+        case 's':
+                singleton = EARGF(usage());
+                break;
+        case 'r':
+                reverse = 0;
+                break;
+        case 'n':
+                natural = 1;
+                reverse = 0;
+                break;
+        default:
+                usage();
+                break;
+        } ARGEND;
+
+        user = getlog();
+        if(user == nil || *user == 0)
+                sysfatal("can't read user name");
+
+        if(cflag){
+                if(argc > 0)
+                        creatembox(user, argv[0]);
+                else
+                        creatembox(user, nil);
+                threadexits(0);
+        }
+
+        if(argc)
+                usage();
+
+#if 0 /* jpc */
+        if(access("/mail/fs/ctl", 0) < 0){
+                startedfs = 1;
+                av[0] = "fs";
+                av[1] = "-p";
+                av[2] = 0;
+                system9("/bin/upas/fs", av, -1);
+        }
+#endif
+        if( (upasfs = nsmount("upasfs", nil)) == nil ) {
+                startedfs = 1;
+                av[0] = "fs";
+                av[1] = "-p";
+                av[2] = 0;
+                fscmd = unsharp("#9/bin/upas/fs");
+                system9(fscmd, av, -1);
+        }
+
+fprint(2,"switchmb\n");
+        switchmb(file, singleton);
+fprint(2,"switchmb2\n");
+
+        top.path = s_copy(root);
+
+        for(cp = ctype; cp < ctype+nelem(ctype)-1; cp++)
+                cp->next = cp+1;
+
+        if(singleton != nil){
+                cur = dosingleton(&top, singleton);
+                if(cur == nil){
+                        Bprint(&out, "no message\n");
+                        exitfs(0);
+                }
+                pcmd(nil, cur);
+        } else {
+                cur = ⊤
+                n = dir2message(&top, reverse);
+                if(n < 0)
+                        sysfatal("can't read %s", s_to_c(top.path));
+                Bprint(&out, "%d message%s\n", n, plural(n));
+        }
+
+
+        notify(catchnote);
+        prompt = s_new();
+        for(;;){
+                s_reset(prompt);
+                if(cur == &top)
+                        s_append(prompt, ": ");
+                else {
+                        mkid(prompt, cur);
+                        s_append(prompt, ": ");
+                }
+
+                // leave space at the end of cmd line in case parsecmd needs to
+                // add a space after a '|' or '!'
+                if(readline(s_to_c(prompt), cmdline, sizeof(cmdline)-1) == nil)
+                        break;
+                err = parsecmd(cmdline, &cmd, top.child, cur);
+                if(err != nil){
+                        Bprint(&out, "!%s\n", err);
+                        continue;
+                }
+                if(singleton != nil && cmd.f == icmd){
+                        Bprint(&out, "!illegal command\n");
+                        continue;
+                }
+                interrupted = 0;
+                if(cmd.msgs == nil || cmd.msgs == &top){
+                        x = (*cmd.f)(&cmd, &top);
+                        if(x != nil)
+                                cur = x;
+                } else for(m = cmd.msgs; m != nil; m = m->cmd){
+                        x = m;
+                        if(cmd.delete){
+                                dcmd(&cmd, x);
+
+                                // dp acts differently than all other commands
+                                // since its an old lesk idiom that people love.
+                                // it deletes the current message, moves the current
+                                // pointer ahead one and prints.
+                                if(cmd.f == pcmd){
+                                        if(x->next == nil){
+                                                Bprint(&out, "!address\n");
+                                                cur = x;
+                                                break;
+                                        } else
+                                                x = x->next;
+                                }
+                        }
+                        x = (*cmd.f)(&cmd, x);
+                        if(x != nil)
+                                cur = x;
+                        if(interrupted)
+                                break;
+                        if(singleton != nil && (cmd.delete || cmd.f == dcmd))
+                                qcmd(nil, nil);
+                }
+                if(doflush)
+                        cur = flushdeleted(cur);
+        }
+        qcmd(nil, nil);
+}
+
+//
+// read the message info
+//
+Message*
+file2message(Message *parent, char *name)
+{
+        Message *m;
+        String *path;
+        char *f[10];
+
+        m = mallocz(sizeof(Message), 1);
+        if(m == nil)
+                return nil;
+        m->path = path = extendpath(parent->path, name);
+        m->fileno = atoi(name);
+        m->info = file2string(path, "info");
+        lineize(s_to_c(m->info), f, nelem(f));
+        m->from = f[0];
+        m->to = f[1];
+        m->cc = f[2];
+        m->replyto = f[3];
+        m->date = f[4];
+        m->subject = f[5];
+        m->type = f[6];
+        m->disposition = f[7];
+        m->filename = f[8];
+        m->len = filelen(path, "raw");
+        if(strstr(m->type, "multipart") != nil || strcmp(m->type, "message/rfc822") == 0)
+                dir2message(m, 0);
+        m->parent = parent;
+
+        return m;
+}
+
+void
+freemessage(Message *m)
+{
+        Message *nm, *next;
+
+        for(nm = m->child; nm != nil; nm = next){
+                next = nm->next;
+                freemessage(nm);
+        }
+        s_free(m->path);
+        s_free(m->info);
+        free(m);
+}
+
+//
+//  read a directory into a list of messages
+//
+int
+dir2message(Message *parent, int reverse)
+{
+//jpc        int i, n, fd, highest, newmsgs;
+        int i, n, highest, newmsgs;
+        CFid *fid;
+        
+        Dir *d;
+        Message *first, *last, *m;
+
+/*        fd = open(s_to_c(parent->path), OREAD);
+        if(fd < 0)
+                return -1; jpc */
+        fid = fsopen(upasfs, s_to_c(parent->path), OREAD);
+        if(fid == nil)
+                return -1;
+
+        // count current entries
+        first = parent->child;
+        highest = newmsgs = 0;
+        for(last = parent->child; last != nil && last->next != nil; last = last->next)
+                if(last->fileno > highest)
+                        highest = last->fileno;
+        if(last != nil)
+                if(last->fileno > highest)
+                        highest = last->fileno;
+
+        n = fsdirreadall(fid, &d);
+        fprint(2,"read %d messages\n", n);
+        for(i = 0; i < n; i++){
+                if((d[i].qid.type & QTDIR) == 0)
+                        continue;
+                if(atoi(d[i].name) <= highest)
+                        continue;
+                fprint(2,"calling file2message %d\n", i);
+                m = file2message(parent, d[i].name);
+                // fprint(2,"returned from file2message\n");
+                if(m == nil)
+                        break;
+                newmsgs++;
+                if(reverse){
+                        m->next = first;
+                        if(first != nil)
+                                first->prev = m;
+                        first = m;
+                } else {
+                        if(first == nil)
+                                first = m;
+                        else
+                                last->next = m;
+                        m->prev = last;
+                        last = m;
+                }
+        } 
+        fprint(2,"exiting loop\n");
+        free(d);
+        fprint(2,"close fid\n");
+        fsclose(fid);
+        fprint(2,"fid closed\n");
+        parent->child = first;
+
+        // renumber and file longest from
+        i = 1;
+        longestfrom = 12;
+        for(m = first; m != nil; m = m->next){
+                fprint(2,"m:%x from: %s\n", m, m->from);
+                m->id = natural ? m->fileno : i++;
+                n = strlen(m->from);
+                fprint(2,"in loop\n");
+                if(n > longestfrom)
+                        longestfrom = n;
+        }
+        fprint(2,"exiting dir2message\n");
+
+        return newmsgs;
+}
+
+//
+//  point directly to a message
+//
+Message*
+dosingleton(Message *parent, char *path)
+{
+        char *p, *np;
+        Message *m;
+
+        // walk down to message and read it
+        if(strlen(path) < rootlen)
+                return nil;
+        if(path[rootlen] != '/')
+                return nil;
+        p = path+rootlen+1;
+        np = strchr(p, '/');
+        if(np != nil)
+                *np = 0;
+        m = file2message(parent, p);
+        if(m == nil)
+                return nil;
+        parent->child = m;
+        m->id = 1;
+
+        // walk down to requested component
+        while(np != nil){
+                *np = '/';
+                np = strchr(np+1, '/');
+                if(np != nil)
+                        *np = 0;
+                for(m = m->child; m != nil; m = m->next)
+                        if(strcmp(path, s_to_c(m->path)) == 0)
+                                return m;
+                if(m == nil)
+                        return nil;
+        }
+        return m;
+}
+
+//
+//  read a file into a string
+//
+String*
+file2string(String *dir, char *file)
+{
+        String *s;
+//jpc        int fd, n, m;
+        int n, m;
+        CFid *fid;
+
+        s = extendpath(dir, file);
+// jpc        fd = open(s_to_c(s), OREAD);
+        fid = fsopen(upasfs,s_to_c(s), OREAD);
+        s_grow(s, 512);                        /* avoid multiple reads on info files */
+        s_reset(s);
+//jpc        if(fd < 0)
+        if(fid == nil)
+                return s;
+
+        for(;;){
+                n = s->end - s->ptr;
+                if(n == 0){
+                        s_grow(s, 128);
+                        continue;
+                }
+//jpc                m = read(fd, s->ptr, n);
+                m = fsread(fid, s->ptr, n);
+                if(m <= 0)
+                        break;
+                s->ptr += m;
+                if(m < n)
+                        break;
+        }
+        s_terminate(s);
+//jpc        close(fd);
+        fsclose(fid);
+
+        return s;
+}
+
+//
+//  get the length of a file
+//
+int
+filelen(String *dir, char *file)
+{
+        String *path;
+        Dir *d;
+        int rv;
+
+        path = extendpath(dir, file);
+//jpc        d = dirstat(s_to_c(path));
+        d = fsdirstat(upasfs,s_to_c(path));
+        if(d == nil){
+                s_free(path);
+                return -1;
+        }
+        s_free(path);
+        rv = d->length;
+        free(d);
+        return rv;
+}
+
+//
+//  walk the path name an element
+//
+String*
+extendpath(String *dir, char *name)
+{
+        String *path;
+
+        if(strcmp(s_to_c(dir), ".") == 0)
+                path = s_new();
+        else {
+                path = s_copy(s_to_c(dir));
+                s_append(path, "/");
+        }
+        s_append(path, name);
+        return path;
+}
+
+int
+cistrncmp(char *a, char *b, int n)
+{
+        while(n-- > 0){
+                if(tolower(*a++) != tolower(*b++))
+                        return -1;
+        }
+        return 0;
+}
+
+int
+cistrcmp(char *a, char *b)
+{
+        for(;;){
+                if(tolower(*a) != tolower(*b++))
+                        return -1;
+                if(*a++ == 0)
+                        break;
+        }
+        return 0;
+}
+
+char*
+nosecs(char *t)
+{
+        char *p;
+
+        p = strchr(t, ':');
+        if(p == nil)
+                return t;
+        p = strchr(p+1, ':');
+        if(p != nil)
+                *p = 0;
+        return t;
+}
+
+char *months[12] =
+{
+        "jan", "feb", "mar", "apr", "may", "jun",
+        "jul", "aug", "sep", "oct", "nov", "dec"
+};
+
+int
+month(char *m)
+{
+        int i;
+
+        for(i = 0; i < 12; i++)
+                if(cistrcmp(m, months[i]) == 0)
+                        return i+1;
+        return 1;
+}
+
+enum
+{
+        Yearsecs= 365*24*60*60
+};
+
+void
+cracktime(char *d, char *out, int len)
+{
+        char in[64];
+        char *f[6];
+        int n;
+        Tm tm;
+        long now, then;
+        char *dtime;
+
+        *out = 0;
+        if(d == nil)
+                return;
+        strncpy(in, d, sizeof(in));
+        in[sizeof(in)-1] = 0;
+        n = getfields(in, f, 6, 1, " \t\r\n");
+        if(n != 6){
+                // unknown style
+                snprint(out, 16, "%10.10s", d);
+                return;
+        }
+        now = time(0);
+        memset(&tm, 0, sizeof tm);
+        if(strchr(f[0], ',') != nil && strchr(f[4], ':') != nil){
+                // 822 style
+                tm.year = atoi(f[3])-1900;
+                tm.mon = month(f[2]);
+                tm.mday = atoi(f[1]);
+                dtime = nosecs(f[4]);
+                then = tm2sec(&tm);
+        } else if(strchr(f[3], ':') != nil){
+                // unix style
+                tm.year = atoi(f[5])-1900;
+                tm.mon = month(f[1]);
+                tm.mday = atoi(f[2]);
+                dtime = nosecs(f[3]);
+                then = tm2sec(&tm);
+        } else {
+                then = now;
+                tm = *localtime(now);
+                dtime = "";
+        }
+
+        if(now - then < Yearsecs/2)
+                snprint(out, len, "%2d/%2.2d %s", tm.mon, tm.mday, dtime);
+        else
+                snprint(out, len, "%2d/%2.2d  %4d", tm.mon, tm.mday, tm.year+1900);
+}
+
+Ctype*
+findctype(Message *m)
+{
+        char *p;
+        char ftype[128];
+        int n, pfd[2];
+        Ctype *a, *cp;
+        /* static Ctype nulltype        = { "", 0, 0, 0 }; jpc */
+        static Ctype bintype         = { "application/octet-stream", "bin", 0, 0 };
+
+        for(cp = ctype; cp; cp = cp->next)
+                if(strncmp(cp->type, m->type, strlen(cp->type)) == 0)
+                        return cp;
+
+/*        use file(1) for any unknown mimetypes
+ *
+ *        if (strcmp(m->type, bintype.type) != 0)
+ *                return &nulltype;
+ */
+        if(pipe(pfd) < 0)
+                return &bintype;
+
+        *ftype = 0;
+        switch(fork()){
+        case -1:
+                break;
+        case 0:
+                close(pfd[1]);
+                close(0);
+                dup(pfd[0], 0);
+                close(1);
+                dup(pfd[0], 1);
+//jpc                 execl("/bin/file", "file", "-m", s_to_c(extendpath(m->path, "body")), nil);
+                execl(unsharp("#9/bin/file"), "file", "-m", s_to_c(extendpath(m->path, "body")), nil);
+                threadexits(0);
+        default:
+                close(pfd[0]);
+                n = read(pfd[1], ftype, sizeof(ftype));
+                if(n > 0)
+                        ftype[n] = 0;
+                close(pfd[1]);
+                waitpid();
+                break;
+        }
+
+        if (*ftype=='\0' || (p = strchr(ftype, '/')) == nil)
+                return &bintype;
+        *p++ = 0;
+
+        a = mallocz(sizeof(Ctype), 1);
+        a->type = strdup(ftype);
+        a->ext = strdup(p);
+        a->display = 0;
+        a->plumbdest = strdup(ftype);
+        for(cp = ctype; cp->next; cp = cp->next)
+                continue;
+        cp->next = a;
+        a->next = nil;
+        return a;
+}
+
+void
+mkid(String *s, Message *m)
+{
+        char buf[32];
+
+        if(m->parent != &top){
+                mkid(s, m->parent);
+                s_append(s, ".");
+        }
+        sprint(buf, "%d", m->id);
+        s_append(s, buf);
+}
+
+void
+snprintheader(char *buf, int len, Message *m)
+{
+        char timebuf[32];
+        String *id;
+        char *p, *q;;
+
+        // create id
+        id = s_new();
+        mkid(id, m);
+
+        if(*m->from == 0){
+                // no from
+                snprint(buf, len, "%-3s    %s %6d  %s",
+                        s_to_c(id),
+                        m->type,
+                        m->len,
+                        m->filename);
+        } else if(*m->subject){
+                q = p = strdup(m->subject);
+                while(*p == ' ')
+                        p++;
+                if(strlen(p) > 50)
+                        p[50] = 0;
+                cracktime(m->date, timebuf, sizeof(timebuf));
+                snprint(buf, len, "%-3s %c%c%c %6d  %11.11s %-*.*s %s",
+                        s_to_c(id),
+                        m->child ? 'H' : ' ',
+                        m->deleted ? 'd' : ' ',
+                        m->stored ? 's' : ' ',
+                        m->len,
+                        timebuf,
+                        longestfrom, longestfrom, m->from,
+                        p);
+                free(q);
+        } else {
+                cracktime(m->date, timebuf, sizeof(timebuf));
+                snprint(buf, len, "%-3s %c%c%c %6d  %11.11s %s",
+                        s_to_c(id),
+                        m->child ? 'H' : ' ',
+                        m->deleted ? 'd' : ' ',
+                        m->stored ? 's' : ' ',
+                        m->len,
+                        timebuf,
+                        m->from);
+        }
+        s_free(id);
+}
+
+char *spaces = "                                                                    ";
+
+void
+snprintHeader(char *buf, int len, int indent, Message *m)
+{
+        String *id;
+        char typeid[64];
+        char *p, *e;
+
+        // create id
+        id = s_new();
+        mkid(id, m);
+
+        e = buf + len;
+
+        snprint(typeid, sizeof typeid, "%s    %s", s_to_c(id), m->type);
+        if(indent < 6)
+                p = seprint(buf, e, "%-32s %-6d ", typeid, m->len);
+        else
+                p = seprint(buf, e, "%-64s %-6d ", typeid, m->len);
+        if(m->filename && *m->filename)
+                p = seprint(p, e, "(file,%s)", m->filename);
+        if(m->from && *m->from)
+                p = seprint(p, e, "(from,%s)", m->from);
+        if(m->subject && *m->subject)
+                seprint(p, e, "(subj,%s)", m->subject);
+
+        s_free(id);
+}
+
+char sstring[256];
+
+//        cmd := range cmd ' ' arg-list ; 
+//        range := address
+//                | address ',' address
+//                | 'g' search ;
+//        address := msgno
+//                | search ;
+//        msgno := number
+//                | number '/' msgno ;
+//        search := '/' string '/'
+//                | '%' string '%' ;
+//
+Reprog*
+parsesearch(char **pp)
+{
+        char *p, *np;
+        int c, n;
+
+        p = *pp;
+        c = *p++;
+        np = strchr(p, c);
+        if(np != nil){
+                *np++ = 0;
+                *pp = np;
+        } else {
+                n = strlen(p);
+                *pp = p + n;
+        }
+        if(*p == 0)
+                p = sstring;
+        else{
+                strncpy(sstring, p, sizeof(sstring));
+                sstring[sizeof(sstring)-1] = 0;
+        }
+        return regcomp(p);
+}
+
+char*
+parseaddr(char **pp, Message *first, Message *cur, Message *unspec, Message **mp)
+{
+        int n;
+        Message *m;
+        char *p;
+        Reprog *prog;
+        int c, sign;
+        char buf[256];
+
+        *mp = nil;
+        p = *pp;
+
+        if(*p == '+'){
+                sign = 1;
+                p++;
+                *pp = p;
+        } else if(*p == '-'){
+                sign = -1;
+                p++;
+                *pp = p;
+        } else
+                sign = 0;
+
+        switch(*p){
+        default:
+                if(sign){
+                        n = 1;
+                        goto number;
+                }
+                *mp = unspec;
+                break;        
+        case '0': case '1': case '2': case '3': case '4':
+        case '5': case '6': case '7': case '8': case '9':
+                n = strtoul(p, pp, 10);
+                if(n == 0){
+                        if(sign)
+                                *mp = cur;
+                        else
+                                *mp = ⊤
+                        break;
+                }
+        number:
+                m = nil;
+                switch(sign){
+                case 0:
+                        for(m = first; m != nil; m = m->next)
+                                if(m->id == n)
+                                        break;
+                        break;
+                case -1:
+                        if(cur != &top)
+                                for(m = cur; m != nil && n > 0; n--)
+                                        m = m->prev;
+                        break;
+                case 1:
+                        if(cur == &top){
+                                n--;
+                                cur = first;
+                        }
+                        for(m = cur; m != nil && n > 0; n--)
+                                m = m->next;
+                        break;
+                }
+                if(m == nil)
+                        return "address";
+                *mp = m;
+                break;
+        case '%':
+        case '/':
+        case '?':
+                c = *p;
+                prog = parsesearch(pp);
+                if(prog == nil)
+                        return "badly formed regular expression";
+                m = nil;
+                switch(c){
+                case '%':
+                        for(m = cur == &top ? first : cur->next; m != nil; m = m->next){
+                                if(rawsearch(m, prog))
+                                        break;
+                        }
+                        break;
+                case '/':
+                        for(m = cur == &top ? first : cur->next; m != nil; m = m->next){
+                                snprintheader(buf, sizeof(buf), m);
+                                if(regexec(prog, buf, nil, 0))
+                                        break;
+                        }
+                        break;
+                case '?':
+                        for(m = cur == &top ? nil : cur->prev; m != nil; m = m->prev){
+                                snprintheader(buf, sizeof(buf), m);
+                                if(regexec(prog, buf, nil, 0))
+                                        break;
+                        }
+                        break;
+                }
+                if(m == nil)
+                        return "search";
+                *mp = m;
+                free(prog);
+                break;
+        case '$':
+                for(m = first; m != nil && m->next != nil; m = m->next)
+                        ;
+                *mp = m;
+                *pp = p+1;
+                break;
+        case '.':
+                *mp = cur;
+                *pp = p+1;
+                break;
+        case ',':
+                *mp = first;
+                *pp = p;
+                break;
+        }
+
+        if(*mp != nil && **pp == '.'){
+                (*pp)++;
+                if((*mp)->child == nil)
+                        return "no sub parts";
+                return parseaddr(pp, (*mp)->child, (*mp)->child, (*mp)->child, mp);
+        }
+        if(**pp == '+' || **pp == '-' || **pp == '/' || **pp == '%')
+                return parseaddr(pp, first, *mp, *mp, mp);
+
+        return nil;
+}
+
+//
+//  search a message for a regular expression match
+//
+int
+rawsearch(Message *m, Reprog *prog)
+{
+        char buf[4096+1];
+//jpc        int i, fd, rv;
+        int i, rv;
+        CFid *fid;
+        String *path;
+
+        path = extendpath(m->path, "raw");
+//jpc        fd = open(s_to_c(path), OREAD);
+//jpc        if(fd < 0)
+//jpc                return 0;
+        fid = fsopen(upasfs,s_to_c(path), OREAD);
+        if(fid == nil)
+                return 0;
+
+        // march through raw message 4096 bytes at a time
+        // with a 128 byte overlap to chain the re search.
+        rv = 0;
+        for(;;){
+//jpc                i = read(fd, buf, sizeof(buf)-1);
+                i = fsread(fid, buf, sizeof(buf)-1);
+                if(i <= 0)
+                        break;
+                buf[i] = 0;
+                if(regexec(prog, buf, nil, 0)){
+                        rv = 1;
+                        break;
+                }
+                if(i < sizeof(buf)-1)
+                        break;
+//jpc                if(seek(fd, -128LL, 1) < 0)
+                if(fsseek(fid, -128LL, 1) < 0)
+                        break;
+        }
+
+        fsclose(fid);
+        s_free(path);
+        return rv;
+}
+
+
+char*
+parsecmd(char *p, Cmd *cmd, Message *first, Message *cur)
+{
+        Reprog *prog;
+        Message *m, *s, *e, **l, *last;
+        char buf[256];
+        char *err;
+        int i, c;
+        char *q;
+        static char errbuf[Errlen];
+
+        cmd->delete = 0;
+        l = &cmd->msgs;
+        *l = nil;
+
+        // eat white space
+        while(*p == ' ')
+                p++;
+
+        // null command is a special case (advance and print)
+        if(*p == 0){
+                if(cur == &top){
+                        // special case
+                        m = first;
+                } else {
+                        // walk to the next message even if we have to go up
+                        m = cur->next;
+                        while(m == nil && cur->parent != nil){
+                                cur = cur->parent;
+                                m = cur->next;
+                        }
+                }
+                if(m == nil)
+                        return "address";
+                *l = m;
+                m->cmd = nil;
+                cmd->an = 0;
+                cmd->f = pcmd;
+                return nil;
+        }
+
+        // global search ?
+        if(*p == 'g'){
+                p++;
+
+                // no search string means all messages
+                if(*p != '/' && *p != '%'){
+                        for(m = first; m != nil; m = m->next){
+                                *l = m;
+                                l = &m->cmd;
+                                *l = nil;
+                        }
+                } else {
+                        // mark all messages matching this search string
+                        c = *p;
+                        prog = parsesearch(&p);
+                        if(prog == nil)
+                                return "badly formed regular expression";
+                        if(c == '%'){
+                                for(m = first; m != nil; m = m->next){
+                                        if(rawsearch(m, prog)){
+                                                *l = m;
+                                                l = &m->cmd;
+                                                *l = nil;
+                                        }
+                                }
+                        } else {
+                                for(m = first; m != nil; m = m->next){
+                                        snprintheader(buf, sizeof(buf), m);
+                                        if(regexec(prog, buf, nil, 0)){
+                                                *l = m;
+                                                l = &m->cmd;
+                                                *l = nil;
+                                        }
+                                }
+                        }
+                        free(prog);
+                }
+        } else {
+        
+                // parse an address
+                s = e = nil;
+                err = parseaddr(&p, first, cur, cur, &s);
+                if(err != nil)
+                        return err;
+                if(*p == ','){
+                        // this is an address range
+                        if(s == &top)
+                                s = first;
+                        p++;
+                        for(last = s; last != nil && last->next != nil; last = last->next)
+                                ;
+                        err = parseaddr(&p, first, cur, last, &e);
+                        if(err != nil)
+                                return err;
+        
+                        // select all messages in the range
+                        for(; s != nil; s = s->next){
+                                *l = s;
+                                l = &s->cmd;
+                                *l = nil;
+                                if(s == e)
+                                        break;
+                        }
+                        if(s == nil)
+                                return "null address range";
+                } else {
+                        // single address
+                        if(s != &top){
+                                *l = s;
+                                s->cmd = nil;
+                        }
+                }
+        }
+
+        // insert a space after '!'s and '|'s
+        for(q = p; *q; q++)
+                if(*q != '!' && *q != '|')
+                        break;
+        if(q != p && *q != ' '){
+                memmove(q+1, q, strlen(q)+1);
+                *q = ' ';
+        }
+
+        cmd->an = getfields(p, cmd->av, nelem(cmd->av) - 1, 1, " \t\r\n");
+        if(cmd->an == 0 || *cmd->av[0] == 0)
+                cmd->f = pcmd;
+        else {
+                // hack to allow all messages to start with 'd'
+                if(*(cmd->av[0]) == 'd' && *(cmd->av[0]+1) != 0){
+                        cmd->delete = 1;
+                        cmd->av[0]++;
+                }
+
+                // search command table
+                for(i = 0; cmdtab[i].cmd != nil; i++)
+                        if(strcmp(cmd->av[0], cmdtab[i].cmd) == 0)
+                                break;
+                if(cmdtab[i].cmd == nil)
+                        return "illegal command";
+                if(cmdtab[i].args == 0 && cmd->an > 1){
+                        snprint(errbuf, sizeof(errbuf), "%s doesn't take an argument", cmdtab[i].cmd);
+                        return errbuf;
+                }
+                cmd->f = cmdtab[i].f;
+        }
+        return nil; 
+}
+
+// inefficient read from standard input
+char*
+readline(char *prompt, char *line, int len)
+{
+        char *p, *e;
+        int n;
+
+retry:
+        interrupted = 0;
+        Bprint(&out, "%s", prompt);
+        Bflush(&out);
+        e = line + len;
+        for(p = line; p < e; p++){
+                n = read(0, p, 1);
+                if(n < 0){
+                        if(interrupted)
+                                goto retry;
+                        return nil;
+                }
+                if(n == 0)
+                        return nil;
+                if(*p == '\n')
+                        break;
+        }
+        *p = 0;
+        return line;
+}
+
+void
+messagecount(Message *m)
+{
+        int i;
+
+        i = 0;
+        for(; m != nil; m = m->next)
+                i++;
+        Bprint(&out, "%d message%s\n", i, plural(i));
+}
+
+Message*
+aichcmd(Message *m, int indent)
+{
+        char        hdr[256];
+
+        if(m == &top)
+                return nil;
+
+        snprintHeader(hdr, sizeof(hdr), indent, m);
+        Bprint(&out, "%s\n", hdr);
+        for(m = m->child; m != nil; m = m->next)
+                aichcmd(m, indent+1);
+        return nil;
+}
+
+Message*
+Hcmd(Cmd* dummy, Message *m)
+{
+        if(m == &top)
+                return nil;
+        aichcmd(m, 0);
+        return nil;
+}
+
+Message*
+hcmd(Cmd* dummy, Message *m)
+{
+        char        hdr[256];
+
+        if(m == &top)
+                return nil;
+
+        snprintheader(hdr, sizeof(hdr), m);
+        Bprint(&out, "%s\n", hdr);
+        return nil;
+}
+
+Message*
+bcmd(Cmd* dummy, Message *m)
+{
+        int i;
+        Message *om = m;
+
+        if(m == &top)
+                m = top.child;
+        for(i = 0; i < 10 && m != nil; i++){
+                hcmd(nil, m);
+                om = m;
+                m = m->next;
+        }
+
+        return om;
+}
+
+Message*
+ncmd(Cmd* dummy, Message *m)
+{
+        if(m == &top)
+                return m->child;
+        return m->next;
+}
+
+int
+printpart(String *s, char *part)
+{
+        char buf[4096];
+//jpc        int n, fd, tot;
+        int n, tot;
+        CFid *fid;
+        String *path;
+
+        path = extendpath(s, part);
+//jpc        fd = open(s_to_c(path), OREAD);
+        fid = fsopen(upasfs,s_to_c(path), OREAD);
+        s_free(path);
+//jpc        if(fd < 0){
+        if(fid ==  nil){
+                fprint(2, "!message dissappeared\n");
+                return 0;
+        }
+        tot = 0;
+//jpc         while((n = read(fd, buf, sizeof(buf))) > 0){
+        while((n = fsread(fid, buf, sizeof(buf))) > 0){
+                if(interrupted)
+                        break;
+                if(Bwrite(&out, buf, n) <= 0)
+                        break;
+                tot += n;
+        }
+        fsclose(fid);
+        return tot;
+}
+
+int
+printhtml(Message *m)
+{
+        Cmd c;
+
+        c.an = 3;
+        c.av[1] = unsharp("#9/bin/htmlfmt");
+        c.av[2] = "-l 40 -cutf-8";
+        Bprint(&out, "!%s\n", c.av[1]);
+        Bflush(&out);
+        pipecmd(&c, m);
+        return 0;
+}
+
+Message*
+Pcmd(Cmd* dummy, Message *m)
+{
+        if(m == &top)
+                return ⊤
+        if(m->parent == &top)
+                printpart(m->path, "unixheader");
+        printpart(m->path, "raw");
+        return m;
+}
+
+void
+compress(char *p)
+{
+        char *np;
+        int last;
+
+        last = ' ';
+        for(np = p; *p; p++){
+                if(*p != ' ' || last != ' '){
+                        last = *p;
+                        *np++ = last;
+                }
+        }
+        *np = 0;
+}
+
+Message*
+pcmd(Cmd* dummy, Message *m)
+{
+        Message *nm;
+        Ctype *cp;
+        String *s;
+        char buf[128];
+
+        if(m == &top)
+                return ⊤
+        if(m->parent == &top)
+                printpart(m->path, "unixheader");
+        if(printpart(m->path, "header") > 0)
+                Bprint(&out, "\n");
+        cp = findctype(m);
+        if(cp->display){
+                if(strcmp(m->type, "text/html") == 0)
+                        printhtml(m);
+                else
+                        printpart(m->path, "body");
+        } else if(strcmp(m->type, "multipart/alternative") == 0){
+                for(nm = m->child; nm != nil; nm = nm->next){
+                        cp = findctype(nm);
+                        if(cp->ext != nil && strncmp(cp->ext, "txt", 3) == 0)
+                                break;
+                }
+                if(nm == nil)
+                        for(nm = m->child; nm != nil; nm = nm->next){
+                                cp = findctype(nm);
+                                if(cp->display)
+                                        break;
+                        }
+                if(nm != nil)
+                        pcmd(nil, nm);
+                else
+                        hcmd(nil, m);
+        } else if(strncmp(m->type, "multipart/", 10) == 0){
+                nm = m->child;
+                if(nm != nil){
+                        // always print first part
+                        pcmd(nil, nm);
+
+                        for(nm = nm->next; nm != nil; nm = nm->next){
+                                s = rooted(s_clone(nm->path));
+                                cp = findctype(nm);
+                                snprintHeader(buf, sizeof buf, -1, nm);
+                                compress(buf);
+                                if(strcmp(nm->disposition, "inline") == 0){
+                                        if(cp->ext != nil)
+                                                Bprint(&out, "\n--- %s %s/body.%s\n\n",
+                                                        buf, s_to_c(s), cp->ext);
+                                        else
+                                                Bprint(&out, "\n--- %s %s/body\n\n",
+                                                        buf, s_to_c(s));
+                                        pcmd(nil, nm);
+                                } else {
+                                        if(cp->ext != nil)
+                                                Bprint(&out, "\n!--- %s %s/body.%s\n",
+                                                        buf, s_to_c(s), cp->ext);
+                                        else
+                                                Bprint(&out, "\n!--- %s %s/body\n",
+                                                        buf, s_to_c(s));
+                                }
+                                s_free(s);
+                        }
+                } else {
+                        hcmd(nil, m);
+                }
+        } else if(strcmp(m->type, "message/rfc822") == 0){
+                pcmd(nil, m->child);
+        } else if(plumb(m, cp) >= 0)
+                Bprint(&out, "\n!--- using plumber to display message of type %s\n", m->type);
+        else
+                Bprint(&out, "\n!--- cannot display messages of type %s\n", m->type);
+                
+        return m;
+}
+
+void
+printpartindented(String *s, char *part, char *indent)
+{
+        char *p;
+        String *path;
+        Biobuf *b;
+
+        fprint(2,"printpartindented: fixme\n");
+        path = extendpath(s, part);
+        b = Bopen(s_to_c(path), OREAD);
+        s_free(path);
+        if(b == nil){
+                fprint(2, "!message dissappeared\n");
+                return;
+        }
+        while((p = Brdline(b, '\n')) != nil){
+                if(interrupted)
+                        break;
+                p[Blinelen(b)-1] = 0;
+                if(Bprint(&out, "%s%s\n", indent, p) <= 0)
+                        break;
+        }
+        Bprint(&out, "\n");
+        Bterm(b);
+}
+
+Message*
+quotecmd(Cmd* dummy, Message *m)
+{
+        Message *nm;
+        Ctype *cp;
+
+        if(m == &top)
+                return ⊤
+        Bprint(&out, "\n");
+        if(m->from != nil && *m->from)
+                Bprint(&out, "On %s, %s wrote:\n", m->date, m->from);
+        cp = findctype(m);
+        if(cp->display){
+                printpartindented(m->path, "body", "> ");
+        } else if(strcmp(m->type, "multipart/alternative") == 0){
+                for(nm = m->child; nm != nil; nm = nm->next){
+                        cp = findctype(nm);
+                        if(cp->ext != nil && strncmp(cp->ext, "txt", 3) == 0)
+                                break;
+                }
+                if(nm == nil)
+                        for(nm = m->child; nm != nil; nm = nm->next){
+                                cp = findctype(nm);
+                                if(cp->display)
+                                        break;
+                        }
+                if(nm != nil)
+                        quotecmd(nil, nm);
+        } else if(strncmp(m->type, "multipart/", 10) == 0){
+                nm = m->child;
+                if(nm != nil){
+                        cp = findctype(nm);
+                        if(cp->display || strncmp(m->type, "multipart/", 10) == 0)
+                                quotecmd(nil, nm);
+                }
+        }
+        return m;
+}
+
+// really delete messages
+Message*
+flushdeleted(Message *cur)
+{
+        Message *m, **l;
+        char buf[1024], *p, *e, *msg;
+//jpc        int deld, n, fd;
+        int deld, n;
+        CFid *fid;
+        int i;
+
+        doflush = 0;
+        deld = 0;
+
+//jpc        fd = open("/mail/fs/ctl", ORDWR);
+//jpc        if(fd < 0){
+//jpc                fprint(2, "!can't delete mail, opening /mail/fs/ctl: %r\n");
+//jpc                exitfs(0);
+//jpc        }
+        fid = fsopen(upasfs,"ctl", ORDWR);
+        if(fid == nil){
+                fprint(2, "!can't delete mail, opening /mail/fs/ctl: %r\n");
+                exitfs(0);
+        }
+        e = &buf[sizeof(buf)];
+        p = seprint(buf, e, "delete %s", mbname);
+        n = 0;
+        for(l = &top.child; *l != nil;){
+                m = *l;
+                if(!m->deleted){
+                        l = &(*l)->next;
+                        continue;
+                }
+
+                // don't return a pointer to a deleted message
+                if(m == cur)
+                        cur = m->next;
+
+                deld++;
+                msg = strrchr(s_to_c(m->path), '/');
+                if(msg == nil)
+                        msg = s_to_c(m->path);
+                else
+                        msg++;
+                if(e-p < 10){
+//jpc                        write(fd, buf, p-buf);
+                        fswrite(fid, buf, p-buf);
+                        n = 0;
+                        p = seprint(buf, e, "delete %s", mbname);
+                }
+                p = seprint(p, e, " %s", msg);
+                n++;
+
+                // unchain and free
+                *l = m->next;
+                if(m->next)
+                        m->next->prev = m->prev;
+                freemessage(m);
+        }
+        if(n)
+                fswrite(fid, buf, p-buf);
+//jpc                write(fd, buf, p-buf);
+
+//jpc        close(fd);
+        fsclose(fid);
+
+        if(deld)
+                Bprint(&out, "!%d message%s deleted\n", deld, plural(deld));
+
+        // renumber
+        i = 1;
+        for(m = top.child; m != nil; m = m->next)
+                m->id = natural ? m->fileno : i++;
+
+        // if we're out of messages, go back to first
+        // if no first, return the fake first
+        if(cur == nil){
+                if(top.child)
+                        return top.child;
+                else
+                        return ⊤
+        }
+        return cur;
+}
+
+Message*
+qcmd(Cmd* dummy, Message* dummy2)
+{
+        flushdeleted(nil);
+
+        if(didopen)
+                closemb();
+        Bflush(&out);
+
+        exitfs(0);
+        return nil;        // not reached
+}
+
+Message*
+ycmd(Cmd* dummy, Message *m)
+{
+        doflush = 1;
+
+        return icmd(nil, m);
+}
+
+Message*
+xcmd(Cmd* dummy, Message* dummy2)
+{
+        exitfs(0);
+        return nil;        // not reached
+}
+
+Message*
+eqcmd(Cmd* dummy, Message *m)
+{
+        if(m == &top)
+                Bprint(&out, "0\n");
+        else
+                Bprint(&out, "%d\n", m->id);
+        return nil;
+}
+
+Message*
+dcmd(Cmd* dummy, Message *m)
+{
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+        while(m->parent != &top)
+                m = m->parent;
+        m->deleted = 1;
+        return m;
+}
+
+Message*
+ucmd(Cmd* dummy, Message *m)
+{
+        if(m == &top)
+                return nil;
+        while(m->parent != &top)
+                m = m->parent;
+        if(m->deleted < 0)
+                Bprint(&out, "!can't undelete, already flushed\n");
+        m->deleted = 0;
+        return m;
+}
+
+
+Message*
+icmd(Cmd* dummy, Message *m)
+{
+        int n;
+
+        n = dir2message(&top, reverse);
+        if(n > 0)
+                Bprint(&out, "%d new message%s\n", n, plural(n));
+        return m;
+}
+
+Message*
+helpcmd(Cmd* dummy, Message *m)
+{
+        int i;
+
+        Bprint(&out, "Commands are of the form []  [args]\n");
+        Bprint(&out, " :=  | ','| 'g'\n");
+        Bprint(&out, " := '.' | '$' | '^' |  |  | '+' | '-'\n");
+        Bprint(&out, " := '/''/' | '?''?' | '%%''%%'\n");
+        Bprint(&out, " :=\n");
+        for(i = 0; cmdtab[i].cmd != nil; i++)
+                Bprint(&out, "%s\n", cmdtab[i].help);
+        return m;
+}
+
+int
+tomailer(char **av)
+{
+        Waitmsg *w;
+        int pid, i;
+
+        // start the mailer and get out of the way
+        switch(pid = fork()){
+        case -1:
+                fprint(2, "can't fork: %r\n");
+                return -1;
+        case 0:
+//jpc                Bprint(&out, "!/bin/upas/marshal");
+                Bprint(&out, "!%s",unsharp("#9/bin/upas/marshal"));
+                for(i = 1; av[i]; i++){
+                        if(strchr(av[i], ' ') != nil)
+                                Bprint(&out, " '%s'", av[i]);
+                        else
+                                Bprint(&out, " %s", av[i]);
+                }
+                Bprint(&out, "\n");
+                Bflush(&out);
+                av[0] = "marshal";
+                chdir(wd);
+//jpc                exec("/bin/upas/marshal", av);
+//jpc                fprint(2, "couldn't exec /bin/upas/marshal\n");
+                exec(unsharp("#9/bin/upas/marshal"), av);
+                fprint(2, "couldn't exec %s\n",unsharp("#9/bin/upas/marshal"));
+                threadexits(0);
+        default:
+                w = wait();
+                if(w == nil){
+                        if(interrupted)
+                                postnote(PNPROC, pid, "die");
+                        waitpid();
+                        return -1;
+                }
+                if(w->msg[0]){
+                        fprint(2, "mailer failed: %s\n", w->msg);
+                        free(w);
+                        return -1;
+                }
+                free(w);
+                Bprint(&out, "!\n");
+                break;
+        }
+        return 0;
+}
+
+//
+// like tokenize but obey "" quoting
+//
+int
+tokenize822(char *str, char **args, int max)
+{
+        int na;
+        int intok = 0, inquote = 0;
+
+        if(max <= 0)
+                return 0;        
+        for(na=0; ;str++)
+                switch(*str) {
+                case ' ':
+                case '\t':
+                        if(inquote)
+                                goto Default;
+                        /* fall through */
+                case '\n':
+                        *str = 0;
+                        if(!intok)
+                                continue;
+                        intok = 0;
+                        if(na < max)
+                                continue;
+                        /* fall through */
+                case 0:
+                        return na;
+                case '"':
+                        inquote ^= 1;
+                        /* fall through */
+                Default:
+                default:
+                        if(intok)
+                                continue;
+                        args[na++] = str;
+                        intok = 1;
+                }
+        return 0;        /* can't get here; silence compiler */
+}
+
+Message*
+rcmd(Cmd *c, Message *m)
+{
+        char *av[128];
+        int i, ai = 1;
+        Message *nm;
+        char *addr;
+        String *path = nil;
+        String *rpath;
+        String *subject = nil;
+        String *from;
+
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+
+        addr = nil;
+        for(nm = m; nm != ⊤ nm = nm->parent){
+                 if(*nm->replyto != 0){
+                        addr = nm->replyto;
+                        break;
+                }
+        }
+        if(addr == nil){
+                Bprint(&out, "!no reply address\n");
+                return nil;
+        }
+
+        if(nm == &top){
+                print("!noone to reply to\n");
+                return nil;
+        }
+
+        for(nm = m; nm != ⊤ nm = nm->parent){
+                if(*nm->subject){
+                        av[ai++] = "-s";
+                        subject = addrecolon(nm->subject);
+                        av[ai++] = s_to_c(subject);;
+                        break;
+                }
+        }
+
+        av[ai++] = "-R";
+        rpath = rooted(s_clone(m->path));
+        av[ai++] = s_to_c(rpath);
+
+        if(strchr(c->av[0], 'f') != nil){
+                fcmd(c, m);
+                av[ai++] = "-F";
+        }
+
+        if(strchr(c->av[0], 'R') != nil){
+                av[ai++] = "-t";
+                av[ai++] = "message/rfc822";
+                av[ai++] = "-A";
+                path = rooted(extendpath(m->path, "raw"));
+                av[ai++] = s_to_c(path);
+        }
+
+        for(i = 1; i < c->an && ai < nelem(av)-1; i++)
+                av[ai++] = c->av[i];
+        from = s_copy(addr);
+        ai += tokenize822(s_to_c(from), &av[ai], nelem(av) - ai);
+        av[ai] = 0;
+        if(tomailer(av) < 0)
+                m = nil;
+        s_free(path);
+        s_free(rpath);
+        s_free(subject);
+        s_free(from);
+        return m;
+}
+
+Message*
+mcmd(Cmd *c, Message *m)
+{
+        char **av;
+        int i, ai;
+        String *path;
+
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+
+        if(c->an < 2){
+                fprint(2, "!usage: M list-of addresses\n");
+                return nil;
+        }
+
+        ai = 1;
+        av = malloc(sizeof(char*)*(c->an + 8));
+
+        av[ai++] = "-t";
+        if(m->parent == &top)
+                av[ai++] = "message/rfc822";
+        else
+                av[ai++] = "mime";
+
+        av[ai++] = "-A";
+        path = rooted(extendpath(m->path, "raw"));
+        av[ai++] = s_to_c(path);
+
+        if(strchr(c->av[0], 'M') == nil)
+                av[ai++] = "-n";
+
+        for(i = 1; i < c->an; i++)
+                av[ai++] = c->av[i];
+        av[ai] = 0;
+
+        if(tomailer(av) < 0)
+                m = nil;
+        if(path != nil)
+                s_free(path);
+        free(av);
+        return m;
+}
+
+Message*
+acmd(Cmd *c, Message *m)
+{
+        char *av[128];
+        int i, ai;
+        String *from, *to, *cc, *path = nil, *subject = nil;
+
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+
+        ai = 1;
+        if(*m->subject){
+                av[ai++] = "-s";
+                subject = addrecolon(m->subject);
+                av[ai++] = s_to_c(subject);
+        }
+
+        if(strchr(c->av[0], 'A') != nil){
+                av[ai++] = "-t";
+                av[ai++] = "message/rfc822";
+                av[ai++] = "-A";
+                path = rooted(extendpath(m->path, "raw"));
+                av[ai++] = s_to_c(path);
+        }
+
+        for(i = 1; i < c->an && ai < nelem(av)-1; i++)
+                av[ai++] = c->av[i];
+        from = s_copy(m->from);
+        ai += tokenize822(s_to_c(from), &av[ai], nelem(av) - ai);
+        to = s_copy(m->to);
+        ai += tokenize822(s_to_c(to), &av[ai], nelem(av) - ai);
+        cc = s_copy(m->cc);
+        ai += tokenize822(s_to_c(cc), &av[ai], nelem(av) - ai);
+        av[ai] = 0;
+        if(tomailer(av) < 0)
+                return nil;
+        s_free(from);
+        s_free(to);
+        s_free(cc);
+        s_free(subject);
+        s_free(path);
+        return m;
+}
+
+String *
+relpath(char *path, String *to)
+{
+        if (*path=='/' || strncmp(path, "./", 2) == 0
+                              || strncmp(path, "../", 3) == 0) {
+                to = s_append(to, path);
+        } else if(mbpath) {
+                to = s_append(to, s_to_c(mbpath));
+                to->ptr = strrchr(to->base, '/')+1;
+                s_append(to, path);
+        }
+        return to;
+}
+
+int
+appendtofile(Message *m, char *part, char *base, int mbox)
+{
+        String *file, *h;
+        int in, out, rv;
+
+        file = extendpath(m->path, part);
+        in = open(s_to_c(file), OREAD);
+        if(in < 0){
+                fprint(2, "!message disappeared\n");
+                return -1;
+        }
+
+        s_reset(file);
+
+        relpath(base, file);
+        if(sysisdir(s_to_c(file))){
+                s_append(file, "/");
+                if(m->filename && strchr(m->filename, '/') == nil)
+                        s_append(file, m->filename);
+                else {
+                        s_append(file, "att.XXXXXXXXXXX");
+                        mktemp(s_to_c(file));
+                }
+        }
+        if(mbox)
+                out = open(s_to_c(file), OWRITE);
+        else
+                out = open(s_to_c(file), OWRITE|OTRUNC);
+        if(out < 0){
+                out = create(s_to_c(file), OWRITE, 0666);
+                if(out < 0){
+                        fprint(2, "!can't open %s: %r\n", s_to_c(file));
+                        close(in);
+                        s_free(file);
+                        return -1;
+                }
+        }
+        if(mbox)
+                seek(out, 0, 2);
+
+        // put on a 'From ' line
+        if(mbox){
+                while(m->parent != &top)
+                        m = m->parent;
+                h = file2string(m->path, "unixheader");
+                fprint(out, "%s", s_to_c(h));
+                s_free(h);
+        }
+
+        // copy the message escaping what we have to ad adding newlines if we have to
+        if(mbox)
+                rv = appendfiletombox(in, out);
+        else
+                rv = appendfiletofile(in, out);
+
+        close(in);
+        close(out);
+
+        if(rv >= 0)
+                print("!saved in %s\n", s_to_c(file));
+        s_free(file);
+        return rv;
+}
+
+Message*
+scmd(Cmd *c, Message *m)
+{
+        char *file;
+
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+
+        switch(c->an){
+        case 1:
+                file = "stored";
+                break;
+        case 2:
+                file = c->av[1];
+                break;
+        default:
+                fprint(2, "!usage: s filename\n");
+                return nil;
+        }
+
+        if(appendtofile(m, "raw", file, 1) < 0)
+                return nil;
+
+        m->stored = 1;
+        return m;
+}
+
+Message*
+wcmd(Cmd *c, Message *m)
+{
+        char *file;
+
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+
+        switch(c->an){
+        case 2:
+                file = c->av[1];
+                break;
+        case 1:
+                if(*m->filename == 0){
+                        fprint(2, "!usage: w filename\n");
+                        return nil;
+                }
+                file = strrchr(m->filename, '/');
+                if(file != nil)
+                        file++;
+                else
+                        file = m->filename;
+                break;
+        default:
+                fprint(2, "!usage: w filename\n");
+                return nil;
+        }
+
+        if(appendtofile(m, "body", file, 0) < 0)
+                return nil;
+        m->stored = 1;
+        return m;
+}
+
+char *specialfile[] =
+{
+        "pipeto",
+        "pipefrom",
+        "L.mbox",
+        "forward",
+        "names"
+};
+
+// return 1 if this is a special file
+static int
+special(String *s)
+{
+        char *p;
+        int i;
+
+        p = strrchr(s_to_c(s), '/');
+        if(p == nil)
+                p = s_to_c(s);
+        else
+                p++;
+        for(i = 0; i < nelem(specialfile); i++)
+                if(strcmp(p, specialfile[i]) == 0)
+                        return 1;
+        return 0;
+}
+
+// open the folder using the recipients account name
+static String*
+foldername(char *rcvr)
+{
+        char *p;
+        int c;
+        String *file;
+        Dir *d;
+        int scarey;
+
+        file = s_new();
+        mboxpath("f", user, file, 0);
+        d = dirstat(s_to_c(file));
+
+        // if $mail/f exists, store there, otherwise in $mail
+        s_restart(file);
+        if(d && d->qid.type == QTDIR){
+                scarey = 0;
+                s_append(file, "f/");
+        } else {
+                scarey = 1;
+        }
+        free(d);
+
+        p = strrchr(rcvr, '!');
+        if(p != nil)
+                rcvr = p+1;
+
+        while(*rcvr && *rcvr != '@'){
+                c = *rcvr++;
+                if(c == '/')
+                        c = '_';
+                s_putc(file, c);
+        }
+        s_terminate(file);
+
+        if(scarey && special(file)){
+                fprint(2, "!won't overwrite %s\n", s_to_c(file));
+                s_free(file);
+                return nil;
+        }
+
+        return file;
+}
+
+Message*
+fcmd(Cmd *c, Message *m)
+{
+        String *folder;
+
+        if(c->an > 1){
+                fprint(2, "!usage: f takes no arguments\n");
+                return nil;
+        }
+
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+
+        folder = foldername(m->from);
+        if(folder == nil)
+                return nil;
+
+        if(appendtofile(m, "raw", s_to_c(folder), 1) < 0){
+                s_free(folder);
+                return nil;
+        }
+        s_free(folder);
+
+        m->stored = 1;
+        return m;
+}
+
+void
+system9(char *cmd, char **av, int in)
+{
+        int pid;
+
+        switch(pid=fork()){
+        case -1:
+                return;
+        case 0:
+                if(in >= 0){
+                        close(0);
+                        dup(in, 0);
+                        close(in);
+                }
+                if(wd[0] != 0)
+                        chdir(wd);
+                exec(cmd, av);
+                fprint(2, "!couldn't exec %s\n", cmd);
+                threadexits(0);
+        default:
+                if(in >= 0)
+                        close(in);
+                while(waitpid() < 0){
+                        if(!interrupted)
+                                break;
+                        postnote(PNPROC, pid, "die");
+                        continue;
+                }
+                break;
+        }
+}
+
+Message*
+bangcmd(Cmd *c, Message *m)
+{
+        char cmd[4*1024];
+        char *p, *e;
+        char *av[4];
+        int i;
+
+        cmd[0] = 0;
+        p = cmd;
+        e = cmd+sizeof(cmd);
+        for(i = 1; i < c->an; i++)
+                p = seprint(p, e, "%s ", c->av[i]);
+        av[0] = "rc";
+        av[1] = "-c";
+        av[2] = cmd;
+        av[3] = 0;
+        system9(unsharp("#9/bin/rc"), av, -1);
+        Bprint(&out, "!\n");
+        return m;
+}
+
+Message*
+xpipecmd(Cmd *c, Message *m, char *part)
+{
+        char cmd[128];
+        char *p, *e;
+        char *av[4];
+        String *path;
+//jpc        int i, fd;
+        int i;
+        CFid *fid;
+
+        if(c->an < 2){
+                Bprint(&out, "!usage: | cmd\n");
+                return nil;
+        }
+
+        if(m == &top){
+                Bprint(&out, "!address\n");
+                return nil;
+        }
+
+        path = extendpath(m->path, part);
+//jpc        fd = open(s_to_c(path), OREAD);
+        fid = fsopen(upasfs,s_to_c(path), OREAD);
+        s_free(path);
+//jpc        if(fd < 0){        // compatibility with older upas/fs
+        if(fid == nil){        // compatibility with older upas/fs
+                path = extendpath(m->path, "raw");
+//jpc                fd = open(s_to_c(path), OREAD);
+                fid = fsopen(upasfs,s_to_c(path), OREAD);
+                s_free(path);
+        }
+        if(fid < 0){
+                fprint(2, "!message disappeared\n");
+                return nil;
+        }
+
+        p = cmd;
+        e = cmd+sizeof(cmd);
+        cmd[0] = 0;
+        for(i = 1; i < c->an; i++)
+                p = seprint(p, e, "%s ", c->av[i]);
+        av[0] = "rc";
+        av[1] = "-c";
+        av[2] = cmd;
+        av[3] = 0;
+//        system9("/bin/rc", av, fd);        /* system closes fd */
+        system9(unsharp("#9/bin/rc"), av, 0);
+        fsclose(fid);
+        Bprint(&out, "!\n");
+        return m;
+}
+
+Message*
+pipecmd(Cmd *c, Message *m)
+{
+        return xpipecmd(c, m, "body");
+}
+
+Message*
+rpipecmd(Cmd *c, Message *m)
+{
+        return xpipecmd(c, m, "rawunix");
+}
+
+#if 0 /* jpc */
+void
+closemb(void)
+{
+        int fd;
+
+        fd = open("/mail/fs/ctl", ORDWR);
+        if(fd < 0)
+                sysfatal("can't open /mail/fs/ctl: %r");
+
+        // close current mailbox
+        if(*mbname && strcmp(mbname, "mbox") != 0)
+                fprint(fd, "close %s", mbname);
+
+        close(fd);
+}
+#endif
+void
+closemb(void)
+{
+        CFid *fid;
+        char s[256];
+
+        fid = fsopen(upasfs,"ctl", ORDWR);
+        if(fid == nil)
+                sysfatal("can't open upasfs/ctl: %r");
+
+        // close current mailbox
+        if(*mbname && strcmp(mbname, "mbox") != 0) {
+                snprint(s, 256, "close %s", mbname);
+                fswrite(fid,s,strlen(s));
+        }
+
+//jpc        close(fd);
+        fsclose(fid);
+}
+
+int
+switchmb(char *file, char *singleton)
+{
+        char *p;
+        int n, fd;
+        String *path;
+        char buf[256];
+
+        // if the user didn't say anything and there
+        // is an mbox mounted already, use that one
+        // so that the upas/fs -fdefault default is honored.
+        if(file 
+        || (singleton && access(singleton, 0)<0)
+/*         || (!singleton && access("/mail/fs/mbox", 0)<0)){ jpc */
+        || (!singleton && fsdirstat(upasfs, "upasfs/mbox") )){
+                fprint(2,"can't access /mail/fs/mbox\n");
+                if(file == nil)
+                        file = "mbox";
+
+                // close current mailbox
+                closemb();
+                didopen = 1;
+
+                fd = open("/mail/fs/ctl", ORDWR);
+                if(fd < 0)
+                        sysfatal("can't open /mail/fs/ctl: %r");
+        
+                path = s_new();
+        
+                // get an absolute path to the mail box
+                if(strncmp(file, "./", 2) == 0){
+                        // resolve path here since upas/fs doesn't know
+                        // our working directory
+                        if(getwd(buf, sizeof(buf)-strlen(file)) == nil){
+                                fprint(2, "!can't get working directory: %s\n", buf);
+                                return -1;
+                        }
+                        s_append(path, buf);
+                        s_append(path, file+1);
+                } else {
+                        mboxpath(file, user, path, 0);
+                }
+        
+                // make up a handle to use when talking to fs
+                p = strrchr(file, '/');
+                if(p == nil){
+                        // if its in the mailbox directory, just use the name
+                        strncpy(mbname, file, sizeof(mbname));
+                        mbname[sizeof(mbname)-1] = 0;
+                } else {
+                        // make up a mailbox name
+                        p = strrchr(s_to_c(path), '/');
+                        p++;
+                        if(*p == 0){
+                                fprint(2, "!bad mbox name");
+                                return -1;
+                        }
+                        strncpy(mbname, p, sizeof(mbname));
+                        mbname[sizeof(mbname)-1] = 0;
+                        n = strlen(mbname);
+                        if(n > Elemlen-12)
+                                n = Elemlen-12;
+                        sprint(mbname+n, "%ld", time(0));
+                }
+
+                if(fprint(fd, "open %s %s", s_to_c(path), mbname) < 0){
+                        fprint(2, "!can't 'open %s %s': %r\n", file, mbname);
+                        s_free(path);
+                        return -1;
+                }
+                close(fd);
+        }else
+        if (singleton && access(singleton, 0)==0
+            && strncmp(singleton, "/mail/fs/", 9) == 0){
+                if ((p = strchr(singleton +10, '/')) == nil){
+                        fprint(2, "!bad mbox name");
+                        return -1;
+                }
+                n = p-(singleton+9);
+                strncpy(mbname, singleton+9, n);
+                mbname[n+1] = 0;
+                path = s_reset(nil);
+                mboxpath(mbname, user, path, 0);
+        }else{
+                path = s_reset(nil);
+                mboxpath("mbox", user, path, 0);
+                strcpy(mbname, "mbox");
+        }
+
+        sprint(root, "%s", mbname);
+        if(getwd(wd, sizeof(wd)) == 0)
+                wd[0] = 0;
+        if(singleton == nil && chdir(root) >= 0)
+                strcpy(root, ".");
+        rootlen = strlen(root);
+
+        if(mbpath != nil)
+                s_free(mbpath);
+        mbpath = path;
+        return 0;
+}
+
+// like tokenize but for into lines
+int
+lineize(char *s, char **f, int n)
+{
+        int i;
+
+        for(i = 0; *s && i < n; i++){
+                f[i] = s;
+                s = strchr(s, '\n');
+                if(s == nil)
+                        break;
+                *s++ = 0;
+        }
+        return i;
+}
+
+
+
+String*
+rooted(String *s)
+{
+        static char buf[256];
+
+        if(strcmp(root, ".") != 0)
+                return s;
+        snprint(buf, sizeof(buf), "/mail/fs/%s/%s", mbname, s_to_c(s));
+        s_free(s);
+        return s_copy(buf);
+}
+
+int
+plumb(Message *m, Ctype *cp)
+{
+        String *s;
+        Plumbmsg *pm;
+        static int fd = -2;
+
+        if(cp->plumbdest == nil)
+                return -1;
+
+        if(fd < -1)
+                fd = plumbopen("send", OWRITE);
+        if(fd < 0)
+                return -1;
+
+        pm = mallocz(sizeof(Plumbmsg), 1);
+        pm->src = strdup("mail");
+        if(*cp->plumbdest)
+                pm->dst = strdup(cp->plumbdest);
+        pm->wdir = nil;
+        pm->type = strdup("text");
+        pm->ndata = -1;
+        s = rooted(extendpath(m->path, "body"));
+        if(cp->ext != nil){
+                s_append(s, ".");
+                s_append(s, cp->ext);
+        }
+        pm->data = strdup(s_to_c(s));
+        s_free(s);
+        plumbsend(fd, pm);
+        plumbfree(pm);
+        return 0;
+}
+
+void
+regerror(char* dummy)
+{
+}
+
+String*
+addrecolon(char *s)
+{
+        String *str;
+
+        if(cistrncmp(s, "re:", 3) != 0){
+                str = s_copy("Re: ");
+                s_append(str, s);
+        } else
+                str = s_copy(s);
+        return str;
+}
+
+void
+exitfs(char *rv)
+{
+        if(startedfs) {
+                fsunmount(upasfs);
+                /* unmount(nil, "/mail/fs"); jpc */
+        }
+//jpc chdir("/sys/src/cmd/upas/ned");
+        threadexits(rv);
+}
diff --git a/src/cmd/upas/pop3/mkfile b/src/cmd/upas/pop3/mkfile
t@@ -0,0 +1,16 @@
+
diff --git a/src/cmd/upas/pop3/pop3.c b/src/cmd/upas/pop3/pop3.c
t@@ -0,0 +1,804 @@
+#include "common.h"
+#include 
+#include 
+#include 
+
+typedef struct Cmd Cmd;
+struct Cmd
+{
+        char *name;
+        int needauth;
+        int (*f)(char*);
+};
+
+static void hello(void);
+static int apopcmd(char*);
+static int capacmd(char*);
+static int delecmd(char*);
+static int listcmd(char*);
+static int noopcmd(char*);
+static int passcmd(char*);
+static int quitcmd(char*);
+static int rsetcmd(char*);
+static int retrcmd(char*);
+static int statcmd(char*);
+static int stlscmd(char*);
+static int topcmd(char*);
+static int synccmd(char*);
+static int uidlcmd(char*);
+static int usercmd(char*);
+static char *nextarg(char*);
+static int getcrnl(char*, int);
+static int readmbox(char*);
+static void sendcrnl(char*, ...);
+static int senderr(char*, ...);
+static int sendok(char*, ...);
+#pragma varargck argpos sendcrnl 1
+#pragma varargck argpos senderr 1
+#pragma varargck argpos sendok 1
+
+Cmd cmdtab[] =
+{
+        "apop", 0, apopcmd,
+        "capa", 0, capacmd,
+        "dele", 1, delecmd,
+        "list", 1, listcmd,
+        "noop", 0, noopcmd,
+        "pass", 0, passcmd,
+        "quit", 0, quitcmd,
+        "rset", 0, rsetcmd,
+        "retr", 1, retrcmd,
+        "stat", 1, statcmd,
+        "stls", 0, stlscmd,
+        "sync", 1, synccmd,
+        "top", 1, topcmd,
+        "uidl", 1, uidlcmd,
+        "user", 0, usercmd,
+        0, 0, 0,
+};
+
+static Biobuf in;
+static Biobuf out;
+static int passwordinclear;
+static int didtls;
+
+typedef struct Msg Msg;
+struct Msg 
+{
+        int upasnum;
+        char digest[64];
+        int bytes;
+        int deleted;
+};
+
+static int totalbytes;
+static int totalmsgs;
+static Msg *msg;
+static int nmsg;
+static int loggedin;
+static int debug;
+static uchar *tlscert;
+static int ntlscert;
+static char *peeraddr;
+static char tmpaddr[64];
+
+void
+usage(void)
+{
+        fprint(2, "usage: upas/pop3 [-a authmboxfile] [-d debugfile] [-p]\n");
+        exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+        int fd;
+        char *arg, cmdbuf[1024];
+        Cmd *c;
+
+        rfork(RFNAMEG);
+        Binit(&in, 0, OREAD);
+        Binit(&out, 1, OWRITE);
+
+        ARGBEGIN{
+        case 'a':
+                loggedin = 1;
+                if(readmbox(EARGF(usage())) < 0)
+                        exits(nil);
+                break;
+        case 'd':
+                debug++;
+                if((fd = create(EARGF(usage()), OWRITE, 0666)) >= 0 && fd != 2){
+                        dup(fd, 2);
+                        close(fd);
+                }
+                break;
+        case 'r':
+                strecpy(tmpaddr, tmpaddr+sizeof tmpaddr, EARGF(usage()));
+                if(arg = strchr(tmpaddr, '!'))
+                        *arg = '\0';
+                peeraddr = tmpaddr;
+                break;
+        case 't':
+                tlscert = readcert(EARGF(usage()), &ntlscert);
+                if(tlscert == nil){
+                        senderr("cannot read TLS certificate: %r");
+                        exits(nil);
+                }
+                break;
+        case 'p':
+                passwordinclear = 1;
+                break;
+        }ARGEND
+
+        /* do before TLS */
+        if(peeraddr == nil)
+                peeraddr = remoteaddr(0,0);
+
+        hello();
+
+        while(Bflush(&out), getcrnl(cmdbuf, sizeof cmdbuf) > 0){
+                arg = nextarg(cmdbuf);
+                for(c=cmdtab; c->name; c++)
+                        if(cistrcmp(c->name, cmdbuf) == 0)
+                                break;
+                if(c->name == 0){
+                        senderr("unknown command %s", cmdbuf);
+                        continue;
+                }
+                if(c->needauth && !loggedin){
+                        senderr("%s requires authentication", cmdbuf);
+                        continue;
+                }
+                (*c->f)(arg);
+        }
+        exits(nil);
+}
+
+/* sort directories in increasing message number order */
+static int
+dircmp(void *a, void *b)
+{
+        return atoi(((Dir*)a)->name) - atoi(((Dir*)b)->name);
+}
+
+static int
+readmbox(char *box)
+{
+        int fd, i, n, nd, lines, pid;
+        char buf[100], err[ERRMAX];
+        char *p;
+        Biobuf *b;
+        Dir *d, *draw;
+        Msg *m;
+        Waitmsg *w;
+
+        unmount(nil, "/mail/fs");
+        switch(pid = fork()){
+        case -1:
+                return senderr("can't fork to start upas/fs");
+
+        case 0:
+                close(0);
+                close(1);
+                open("/dev/null", OREAD);
+                open("/dev/null", OWRITE);
+                execl("/bin/upas/fs", "upas/fs", "-np", "-f", box, nil);
+                snprint(err, sizeof err, "upas/fs: %r");
+                _exits(err);
+                break;
+
+        default:
+                break;
+        }
+
+        if((w = wait()) == nil || w->pid != pid || w->msg[0] != '\0'){
+                if(w && w->pid==pid)
+                        return senderr("%s", w->msg);
+                else
+                        return senderr("can't initialize upas/fs");
+        }
+        free(w);
+
+        if(chdir("/mail/fs/mbox") < 0)
+                return senderr("can't initialize upas/fs: %r");
+
+        if((fd = open(".", OREAD)) < 0)
+                return senderr("cannot open /mail/fs/mbox: %r");
+        nd = dirreadall(fd, &d);
+        close(fd);
+        if(nd < 0)
+                return senderr("cannot read from /mail/fs/mbox: %r");
+
+        msg = mallocz(sizeof(Msg)*nd, 1);
+        if(msg == nil)
+                return senderr("out of memory");
+
+        if(nd == 0)
+                return 0;
+        qsort(d, nd, sizeof(d[0]), dircmp);
+
+        for(i=0; iupasnum = atoi(d[i].name);
+                sprint(buf, "%d/digest", m->upasnum);
+                if((fd = open(buf, OREAD)) < 0)
+                        continue;
+                n = readn(fd, m->digest, sizeof m->digest - 1);
+                close(fd);
+                if(n < 0)
+                        continue;
+                m->digest[n] = '\0';
+
+                /*
+                 * We need the number of message lines so that we
+                 * can adjust the byte count to include \r's.
+                 * Upas/fs gives us the number of lines in the raw body
+                 * in the lines file, but we have to count rawheader ourselves.
+                 * There is one blank line between raw header and raw body.
+                 */
+                sprint(buf, "%d/rawheader", m->upasnum);
+                if((b = Bopen(buf, OREAD)) == nil)
+                        continue;
+                lines = 0;
+                for(;;){
+                        p = Brdline(b, '\n');
+                        if(p == nil){
+                                if((n = Blinelen(b)) == 0)
+                                        break;
+                                Bseek(b, n, 1);
+                        }else
+                                lines++;
+                }
+                Bterm(b);
+                lines++;
+                sprint(buf, "%d/lines", m->upasnum);
+                if((fd = open(buf, OREAD)) < 0)
+                        continue;
+                n = readn(fd, buf, sizeof buf - 1);
+                close(fd);
+                if(n < 0)
+                        continue;
+                buf[n] = '\0';
+                lines += atoi(buf);
+
+                sprint(buf, "%d/raw", m->upasnum);
+                if((draw = dirstat(buf)) == nil)
+                        continue;
+                m->bytes = lines+draw->length;
+                free(draw);
+                nmsg++;
+                totalmsgs++;
+                totalbytes += m->bytes;
+        }
+        return 0;
+}
+
+/*
+ *  get a line that ends in crnl or cr, turn terminating crnl into a nl
+ *
+ *  return 0 on EOF
+ */
+static int
+getcrnl(char *buf, int n)
+{
+        int c;
+        char *ep;
+        char *bp;
+        Biobuf *fp = ∈
+
+        Bflush(&out);
+
+        bp = buf;
+        ep = bp + n - 1;
+        while(bp != ep){
+                c = Bgetc(fp);
+                if(debug) {
+                        seek(2, 0, 2);
+                        fprint(2, "%c", c);
+                }
+                switch(c){
+                case -1:
+                        *bp = 0;
+                        if(bp==buf)
+                                return 0;
+                        else
+                                return bp-buf;
+                case '\r':
+                        c = Bgetc(fp);
+                        if(c == '\n'){
+                                if(debug) {
+                                        seek(2, 0, 2);
+                                        fprint(2, "%c", c);
+                                }
+                                *bp = 0;
+                                return bp-buf;
+                        }
+                        Bungetc(fp);
+                        c = '\r';
+                        break;
+                case '\n':
+                        *bp = 0;
+                        return bp-buf;
+                }
+                *bp++ = c;
+        }
+        *bp = 0;
+        return bp-buf;
+}
+
+static void
+sendcrnl(char *fmt, ...)
+{
+        char buf[1024];
+        va_list arg;
+
+        va_start(arg, fmt);
+        vseprint(buf, buf+sizeof(buf), fmt, arg);
+        va_end(arg);
+        if(debug)
+                fprint(2, "-> %s\n", buf);
+        Bprint(&out, "%s\r\n", buf);
+}
+
+static int
+senderr(char *fmt, ...)
+{
+        char buf[1024];
+        va_list arg;
+
+        va_start(arg, fmt);
+        vseprint(buf, buf+sizeof(buf), fmt, arg);
+        va_end(arg);
+        if(debug)
+                fprint(2, "-> -ERR %s\n", buf);
+        Bprint(&out, "-ERR %s\r\n", buf);
+        return -1;
+}
+
+static int
+sendok(char *fmt, ...)
+{
+        char buf[1024];
+        va_list arg;
+
+        va_start(arg, fmt);
+        vseprint(buf, buf+sizeof(buf), fmt, arg);
+        va_end(arg);
+        if(*buf){
+                if(debug)
+                        fprint(2, "-> +OK %s\n", buf);
+                Bprint(&out, "+OK %s\r\n", buf);
+        } else {
+                if(debug)
+                        fprint(2, "-> +OK\n");
+                Bprint(&out, "+OK\r\n");
+        }
+        return 0;
+}
+
+static int
+capacmd(char*)
+{
+        sendok("");
+        sendcrnl("TOP");
+        if(passwordinclear || didtls)
+                sendcrnl("USER");
+        sendcrnl("PIPELINING");
+        sendcrnl("UIDL");
+        sendcrnl("STLS");
+        sendcrnl(".");
+        return 0;
+}
+
+static int
+delecmd(char *arg)
+{
+        int n;
+
+        if(*arg==0)
+                return senderr("DELE requires a message number");
+
+        n = atoi(arg)-1;
+        if(n < 0 || n >= nmsg || msg[n].deleted)
+                return senderr("no such message");
+
+        msg[n].deleted = 1;
+        totalmsgs--;
+        totalbytes -= msg[n].bytes;
+        sendok("message %d deleted", n+1);
+        return 0;
+}
+
+static int
+listcmd(char *arg)
+{
+        int i, n;
+
+        if(*arg == 0){
+                sendok("+%d message%s (%d octets)", totalmsgs, totalmsgs==1 ? "":"s", totalbytes);
+                for(i=0; i= nmsg || msg[n].deleted)
+                        return senderr("no such message");
+                sendok("%d %d", n+1, msg[n].bytes);
+        }
+        return 0;
+}
+
+static int
+noopcmd(char *arg)
+{
+        USED(arg);
+        sendok("");
+        return 0;
+}
+
+static void
+_synccmd(char*)
+{
+        int i, fd;
+        char *s;
+        Fmt f;
+
+        if(!loggedin){
+                sendok("");
+                return;
+        }
+
+        fmtstrinit(&f);
+        fmtprint(&f, "delete mbox");
+        for(i=0; i= nmsg || msg[n].deleted)
+                return senderr("no such message");
+        snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum);
+        if((b = Bopen(buf, OREAD)) == nil)
+                return senderr("message disappeared");
+        sendok("");
+        while((p = Brdstr(b, '\n', 1)) != nil){
+                if(p[0]=='.')
+                        Bwrite(&out, ".", 1);
+                Bwrite(&out, p, strlen(p));
+                Bwrite(&out, "\r\n", 2);
+                free(p);
+        }
+        Bterm(b);
+        sendcrnl(".");
+        return 0;
+}
+
+static int
+rsetcmd(char*)
+{
+        int i;
+
+        for(i=0; i= nmsg || msg[n].deleted)
+                return senderr("no such message");
+        arg = nextarg(arg);
+        if(*arg == 0)
+                return senderr("TOP requires a line count");
+        lines = atoi(arg);
+        if(lines < 0)
+                return senderr("bad args to TOP");
+        snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum);
+        if((b = Bopen(buf, OREAD)) == nil)
+                return senderr("message disappeared");
+        sendok("");
+        while(p = Brdstr(b, '\n', 1)){
+                if(p[0]=='.')
+                        Bputc(&out, '.');
+                Bwrite(&out, p, strlen(p));
+                Bwrite(&out, "\r\n", 2);
+                done = p[0]=='\0';
+                free(p);
+                if(done)
+                        break;
+        }
+        for(i=0; i= nmsg || msg[n].deleted)
+                        return senderr("no such message");
+                sendok("%d %s", n+1, msg[n].digest);
+        }
+        return 0;        
+}
+
+static char*
+nextarg(char *p)
+{
+        while(*p && *p != ' ' && *p != '\t')
+                p++;
+        while(*p == ' ' || *p == '\t')
+                *p++ = 0;
+        return p;
+}
+
+/*
+ * authentication
+ */
+Chalstate *chs;
+char user[256];
+char box[256];
+char cbox[256];
+
+static void
+hello(void)
+{
+        fmtinstall('H', encodefmt);
+        if((chs = auth_challenge("proto=apop role=server")) == nil){
+                senderr("auth server not responding, try later");
+                exits(nil);
+        }
+
+        sendok("POP3 server ready %s", chs->chal);
+}
+
+static int
+setuser(char *arg)
+{
+        char *p;
+
+        strcpy(box, "/mail/box/");
+        strecpy(box+strlen(box), box+sizeof box-7, arg);
+        strcpy(cbox, box);
+        cleanname(cbox);
+        if(strcmp(cbox, box) != 0)
+                return senderr("bad mailbox name");
+        strcat(box, "/mbox");
+
+        strecpy(user, user+sizeof user, arg);
+        if(p = strchr(user, '/'))
+                *p = '\0';
+        return 0;
+}
+
+static int
+usercmd(char *arg)
+{
+        if(loggedin)
+                return senderr("already authenticated");
+        if(*arg == 0)
+                return senderr("USER requires argument");
+        if(setuser(arg) < 0)
+                return -1;
+        return sendok("");
+}
+
+static void
+enableaddr(void)
+{
+        int fd;
+        char buf[64];
+
+        /* hide the peer IP address under a rock in the ratifier FS */
+        if(peeraddr == 0 || *peeraddr == 0)
+                return;
+
+        sprint(buf, "/mail/ratify/trusted/%s#32", peeraddr);
+
+        /*
+         * if the address is already there and the user owns it,
+         * remove it and recreate it to give him a new time quanta.
+         */
+        if(access(buf, 0) >= 0  && remove(buf) < 0)
+                return;
+
+        fd = create(buf, OREAD, 0666);
+        if(fd >= 0){
+                close(fd);
+//                syslog(0, "pop3", "ratified %s", peeraddr);
+        }
+}
+
+static int
+dologin(char *response)
+{
+        AuthInfo *ai;
+        static int tries;
+
+        chs->user = user;
+        chs->resp = response;
+        chs->nresp = strlen(response);
+        if((ai = auth_response(chs)) == nil){
+                if(tries++ >= 5){
+                        senderr("authentication failed: %r; server exiting");
+                        exits(nil);
+                }        
+                return senderr("authentication failed");
+        }
+
+        if(auth_chuid(ai, nil) < 0){
+                senderr("chuid failed: %r; server exiting");
+                exits(nil);
+        }
+        auth_freeAI(ai);
+        auth_freechal(chs);
+        chs = nil;
+
+        loggedin = 1;
+        if(newns(user, 0) < 0){
+                senderr("newns failed: %r; server exiting");
+                exits(nil);
+        }
+
+        enableaddr();
+        if(readmbox(box) < 0)
+                exits(nil);
+        return sendok("mailbox is %s", box);
+}
+
+static int
+passcmd(char *arg)
+{
+        DigestState *s;
+        uchar digest[MD5dlen];
+        char response[2*MD5dlen+1];
+
+        if(passwordinclear==0 && didtls==0)
+                return senderr("password in the clear disallowed");
+
+        /* use password to encode challenge */
+        if((chs = auth_challenge("proto=apop role=server")) == nil)
+                return senderr("couldn't get apop challenge");
+
+        // hash challenge with secret and convert to ascii
+        s = md5((uchar*)chs->chal, chs->nchal, 0, 0);
+        md5((uchar*)arg, strlen(arg), digest, s);
+        snprint(response, sizeof response, "%.*H", MD5dlen, digest);
+        return dologin(response);
+}
+
+static int
+apopcmd(char *arg)
+{
+        char *resp;
+
+        resp = nextarg(arg);
+        if(setuser(arg) < 0)
+                return -1;
+        return dologin(resp);
+}
+
diff --git a/src/cmd/upas/q/mkfile b/src/cmd/upas/q/mkfile
t@@ -0,0 +1,22 @@
+<$PLAN9/src/mkhdr
+
+TARG = qer\
+        runq\
+
+OFILES=
+
+HFILES=../common/common.h\
+        ../common/sys.h\
+
+LIB=../common/libcommon.a\
+
+BIN=$PLAN9/bin/upas
+
+UPDATE=\
+        mkfile\
+        $HFILES\
+        ${OFILES:%.$O=%.c}\
+        ${TARG:%=%.c}\
+
+<$PLAN9/src/mkmany
+CFLAGS=$CFLAGS -I../common
diff --git a/src/cmd/upas/q/qer.c b/src/cmd/upas/q/qer.c
t@@ -0,0 +1,193 @@
+#include "common.h"
+
+typedef struct Qfile Qfile;
+struct Qfile
+{
+        Qfile        *next;
+        char        *name;
+        char        *tname;
+} *files;
+
+char *user;
+int isnone;
+
+int        copy(Qfile*);
+
+void
+usage(void)
+{
+        fprint(2, "usage: qer [-f file] [-q dir] q-root description reply-to arg-list\n");
+        exits("usage");
+}
+
+void
+error(char *f, char *a)
+{
+        char err[Errlen+1];
+        char buf[256];
+
+        rerrstr(err, sizeof(err));
+        snprint(buf, sizeof(buf),  f, a);
+        fprint(2, "qer: %s: %s\n", buf, err);
+        exits(buf);
+}
+
+void
+main(int argc, char**argv)
+{
+        Dir        *dir;
+        String        *f, *c;
+        int        fd;
+        char        file[1024];
+        char        buf[1024];
+        long        n;
+        char        *cp, *qdir;
+        int        i;
+        Qfile        *q, **l;
+
+        l = &files;
+        qdir = 0;
+
+        ARGBEGIN {
+        case 'f':
+                q = malloc(sizeof(Qfile));
+                q->name = ARGF();
+                q->next = *l;
+                *l = q;
+                break;
+        case 'q':
+                qdir = ARGF();
+                if(qdir == 0)
+                        usage();
+                break;
+        default:
+                usage();
+        } ARGEND;
+
+        if(argc < 3)
+                usage();
+        user = getuser();
+        isnone = (qdir != 0) || (strcmp(user, "none") == 0);
+
+        if(qdir == 0) {
+                qdir = user;
+                if(qdir == 0)
+                        error("unknown user", 0);
+        }
+        snprint(file, sizeof(file), "%s/%s", argv[0], qdir);
+
+        /*
+         *  data file name
+         */
+        f = s_copy(file);
+        s_append(f, "/D.XXXXXX");
+        mktemp(s_to_c(f));
+        cp = utfrrune(s_to_c(f), '/');
+        cp++;
+
+        /*
+         *  create directory and data file.  once the data file
+         *  exists, runq won't remove the directory
+         */
+        fd = -1;
+        for(i = 0; i < 10; i++){
+                int perm;
+
+                dir = dirstat(file);
+                if(dir == nil){
+                        perm = isnone?0777:0775;
+                        if(sysmkdir(file, perm) < 0)
+                                continue;
+                } else {
+                        if((dir->qid.type&QTDIR)==0)
+                                error("not a directory %s", file);
+                }
+                perm = isnone?0664:0660;
+                fd = create(s_to_c(f), OWRITE, perm);
+                if(fd >= 0)
+                        break;
+                sleep(250);
+        }
+        if(fd < 0)
+                error("creating data file %s", s_to_c(f));
+
+        /*
+         *  copy over associated files
+         */
+        if(files){
+                *cp = 'F';
+                for(q = files; q; q = q->next){
+                        q->tname = strdup(s_to_c(f));
+                        if(copy(q) < 0)
+                                error("copying %s to queue", q->name);
+                        (*cp)++;
+                }
+        }
+
+        /*
+         *  copy in the data file
+         */
+        i = 0;
+        while((n = read(0, buf, sizeof(buf)-1)) > 0){
+                if(i++ == 0 && strncmp(buf, "From", 4) != 0){
+                        buf[n] = 0;
+                        syslog(0, "smtp", "qer usys data starts with %-40.40s\n", buf);
+                }
+                if(write(fd, buf, n) != n)
+                        error("writing data file %s", s_to_c(f));
+        }
+/*        if(n < 0)
+                error("reading input"); */
+        close(fd);
+
+        /*
+         *  create control file
+         */
+        *cp = 'C';
+        fd = syscreatelocked(s_to_c(f), OWRITE, 0664);
+        if(fd < 0)
+                error("creating control file %s", s_to_c(f));
+        c = s_new();
+        for(i = 1; i < argc; i++){
+                s_append(c, argv[i]);
+                s_append(c, " ");
+        }
+        for(q = files; q; q = q->next){
+                s_append(c, q->tname);
+                s_append(c, " ");
+        }
+        s_append(c, "\n");
+        if(write(fd, s_to_c(c), strlen(s_to_c(c))) < 0) {
+                sysunlockfile(fd);
+                error("writing control file %s", s_to_c(f));
+        }
+        sysunlockfile(fd);
+        exits(0);
+}
+
+int
+copy(Qfile *q)
+{
+        int from, to, n;
+        char buf[4096];
+
+        from = open(q->name, OREAD);
+        if(from < 0)
+                return -1;
+        to = create(q->tname, OWRITE, 0660);
+        if(to < 0){
+                close(from);
+                return -1;
+        }
+        for(;;){
+                n = read(from, buf, sizeof(buf));
+                if(n <= 0)
+                        break;
+                n = write(to, buf, n);
+                if(n < 0)
+                        break;
+        }
+        close(to);
+        close(from);
+        return n;
+}
diff --git a/src/cmd/upas/q/runq.c b/src/cmd/upas/q/runq.c
t@@ -0,0 +1,766 @@
+#include "common.h"
+#include 
+
+void        doalldirs(void);
+void        dodir(char*);
+void        dofile(Dir*);
+void        rundir(char*);
+char*        file(char*, char);
+void        warning(char*, void*);
+void        error(char*, void*);
+int        returnmail(char**, char*, char*);
+void        logit(char*, char*, char**);
+void        doload(int);
+
+#define HUNK 32
+char        *cmd;
+char        *root;
+int        debug;
+int        giveup = 2*24*60*60;
+int        load;
+int        limit;
+
+/* the current directory */
+Dir        *dirbuf;
+long        ndirbuf = 0;
+int        nfiles;
+char        *curdir;
+
+char *runqlog = "runq";
+
+int        *pidlist;
+char        **badsys;                /* array of recalcitrant systems */
+int        nbad;
+int        npid = 50;
+int        sflag;                        /* single thread per directory */
+int        aflag;                        /* all directories */
+int        Eflag;                        /* ignore E.xxxxxx dates */
+int        Rflag;                        /* no giving up, ever */
+
+void
+usage(void)
+{
+        fprint(2, "usage: runq [-adsE] [-q dir] [-l load] [-t time] [-r nfiles] [-n nprocs] q-root cmd\n");
+        exits("");
+}
+
+void
+main(int argc, char **argv)
+{
+        char *qdir, *x;
+
+        qdir = 0;
+
+        ARGBEGIN{
+        case 'l':
+                x = ARGF();
+                if(x == 0)
+                        usage();
+                load = atoi(x);
+                if(load < 0)
+                        load = 0;
+                break;
+        case 'E':
+                Eflag++;
+                break;
+        case 'R':        /* no giving up -- just leave stuff in the queue */
+                Rflag++;
+                break;
+        case 'a':
+                aflag++;
+                break;
+        case 'd':
+                debug++;
+                break;
+        case 'r':
+                limit = atoi(ARGF());
+                break;
+        case 's':
+                sflag++;
+                break;
+        case 't':
+                giveup = 60*60*atoi(ARGF());
+                break;
+        case 'q':
+                qdir = ARGF();
+                if(qdir == 0)
+                        usage();
+                break;
+        case 'n':
+                npid = atoi(ARGF());
+                if(npid == 0)
+                        usage();
+                break;
+        }ARGEND;
+
+        if(argc != 2)
+                usage();
+
+        pidlist = malloc(npid*sizeof(*pidlist));
+        if(pidlist == 0)
+                error("can't malloc", 0);
+
+        if(aflag == 0 && qdir == 0) {
+                qdir = getuser();
+                if(qdir == 0)
+                        error("unknown user", 0);
+        }
+        root = argv[0];
+        cmd = argv[1];
+
+        if(chdir(root) < 0)
+                error("can't cd to %s", root);
+
+        doload(1);
+        if(aflag)
+                doalldirs();
+        else
+                dodir(qdir);
+        doload(0);
+        exits(0);
+}
+
+int
+emptydir(char *name)
+{
+        int fd;
+        long n;
+        char buf[2048];
+
+        fd = open(name, OREAD);
+        if(fd < 0)
+                return 1;
+        n = read(fd, buf, sizeof(buf));
+        close(fd);
+        if(n <= 0) {
+                if(debug)
+                        fprint(2, "removing directory %s\n", name);
+                syslog(0, runqlog, "rmdir %s", name);
+                sysremove(name);
+                return 1;
+        }
+        return 0;
+}
+
+int
+forkltd(void)
+{
+        int i;
+        int pid;
+
+        for(i = 0; i < npid; i++){
+                if(pidlist[i] <= 0)
+                        break;
+        }
+
+        while(i >= npid){
+                pid = waitpid();
+                if(pid < 0){
+                        syslog(0, runqlog, "forkltd confused");
+                        exits(0);
+                }
+
+                for(i = 0; i < npid; i++)
+                        if(pidlist[i] == pid)
+                                break;
+        }
+        pidlist[i] = fork();
+        return pidlist[i];
+}
+
+/*
+ *  run all user directories, must be bootes (or root on unix) to do this
+ */
+void
+doalldirs(void)
+{
+        Dir *db;
+        int fd;
+        long i, n;
+
+
+        fd = open(".", OREAD);
+        if(fd == -1){
+                warning("reading %s", root);
+                return;
+        }
+        n = sysdirreadall(fd, &db);
+        if(n > 0){
+                for(i=0; i 0){
+                for(i=0; ifd = fd;
+        l->name = s_new();
+        s_append(l->name, path);
+
+        /* fork process to keep lock alive until sysunlock(l) */
+        switch(l->pid = rfork(RFPROC)){
+        default:
+                break;
+        case 0:
+                fd = l->fd;
+                for(;;){
+                        sleep(1000*60);
+                        if(pread(fd, buf, 1, 0) < 0)
+                                break;
+                }
+                _exits(0);
+        }
+        return l;
+}
+
+/*
+ *  try a message
+ */
+void
+dofile(Dir *dp)
+{
+        Dir *d;
+        int dfd, ac, dtime, efd, pid, i, etime;
+        char *buf, *cp, **av;
+        Waitmsg *wm;
+        Biobuf *b;
+        Mlock *l = nil;
+
+        if(debug)
+                fprint(2, "dofile %s\n", dp->name);
+        /*
+         *  if no data file or empty control or data file, just clean up
+         *  the empty control file must be 15 minutes old, to minimize the
+         *  chance of a race.
+         */
+        d = dirstat(file(dp->name, 'D'));
+        if(d == nil){
+                syslog(0, runqlog, "no data file for %s", dp->name);
+                remmatch(dp->name);
+                return;
+        }
+        if(dp->length == 0){
+                if(time(0)-dp->mtime > 15*60){
+                        syslog(0, runqlog, "empty ctl file for %s", dp->name);
+                        remmatch(dp->name);
+                }
+                return;
+        }
+        dtime = d->mtime;
+        free(d);
+
+        /*
+         *  retry times depend on the age of the errors file
+         */
+        if(!Eflag && (d = dirstat(file(dp->name, 'E'))) != nil){
+                etime = d->mtime;
+                free(d);
+                if(etime - dtime < 60*60){
+                        /* up to the first hour, try every 15 minutes */
+                        if(time(0) - etime < 15*60)
+                                return;
+                } else {
+                        /* after the first hour, try once an hour */
+                        if(time(0) - etime < 60*60)
+                                return;
+                }
+
+        }
+
+        /*
+         *  open control and data
+         */
+        b = sysopen(file(dp->name, 'C'), "rl", 0660);
+        if(b == 0) {
+                if(debug)
+                        fprint(2, "can't open %s: %r\n", file(dp->name, 'C'));
+                return;
+        }
+        dfd = open(file(dp->name, 'D'), OREAD);
+        if(dfd < 0){
+                if(debug)
+                        fprint(2, "can't open %s: %r\n", file(dp->name, 'D'));
+                Bterm(b);
+                sysunlockfile(Bfildes(b));
+                return;
+        }
+
+        /*
+         *  make arg list
+         *        - read args into (malloc'd) buffer
+         *        - malloc a vector and copy pointers to args into it
+         */
+        buf = malloc(dp->length+1);
+        if(buf == 0){
+                warning("buffer allocation", 0);
+                Bterm(b);
+                sysunlockfile(Bfildes(b));
+                close(dfd);
+                return;
+        }
+        if(Bread(b, buf, dp->length) != dp->length){
+                warning("reading control file %s\n", dp->name);
+                Bterm(b);
+                sysunlockfile(Bfildes(b));
+                close(dfd);
+                free(buf);
+                return;
+        }
+        buf[dp->length] = 0;
+        av = malloc(2*sizeof(char*));
+        if(av == 0){
+                warning("argv allocation", 0);
+                close(dfd);
+                free(buf);
+                Bterm(b);
+                sysunlockfile(Bfildes(b));
+                return;
+        }
+        for(ac = 1, cp = buf; *cp; ac++){
+                while(isspace(*cp))
+                        *cp++ = 0;
+                if(*cp == 0)
+                        break;
+
+                av = realloc(av, (ac+2)*sizeof(char*));
+                if(av == 0){
+                        warning("argv allocation", 0);
+                        close(dfd);
+                        free(buf);
+                        Bterm(b);
+                        sysunlockfile(Bfildes(b));
+                        return;
+                }
+                av[ac] = cp;
+                while(*cp && !isspace(*cp)){
+                        if(*cp++ == '"'){
+                                while(*cp && *cp != '"')
+                                        cp++;
+                                if(*cp)
+                                        cp++;
+                        }
+                }
+        }
+        av[0] = cmd;
+        av[ac] = 0;
+
+        if(!Eflag &&time(0) - dtime > giveup){
+                if(returnmail(av, dp->name, "Giveup") != 0)
+                        logit("returnmail failed", dp->name, av);
+                remmatch(dp->name);
+                goto done;
+        }
+
+        for(i = 0; i < nbad; i++){
+                if(strcmp(av[3], badsys[i]) == 0)
+                        goto done;
+        }
+
+        /*
+         * Ken's fs, for example, gives us 5 minutes of inactivity before
+         * the lock goes stale, so we have to keep reading it.
+          */
+        l = keeplockalive(file(dp->name, 'C'), Bfildes(b));
+
+        /*
+         *  transfer
+         */
+        pid = fork();
+        switch(pid){
+        case -1:
+                sysunlock(l);
+                sysunlockfile(Bfildes(b));
+                syslog(0, runqlog, "out of procs");
+                exits(0);
+        case 0:
+                if(debug) {
+                        fprint(2, "Starting %s", cmd);
+                        for(ac = 0; av[ac]; ac++)
+                                fprint(2, " %s", av[ac]);
+                        fprint(2, "\n");
+                }
+                logit("execing", dp->name, av);
+                close(0);
+                dup(dfd, 0);
+                close(dfd);
+                close(2);
+                efd = open(file(dp->name, 'E'), OWRITE);
+                if(efd < 0){
+                        if(debug) syslog(0, "runq", "open %s as %s: %r", file(dp->name,'E'), getuser());
+                        efd = create(file(dp->name, 'E'), OWRITE, 0666);
+                        if(efd < 0){
+                                if(debug) syslog(0, "runq", "create %s as %s: %r", file(dp->name, 'E'), getuser());
+                                exits("could not open error file - Retry");
+                        }
+                }
+                seek(efd, 0, 2);
+                exec(cmd, av);
+                error("can't exec %s", cmd);
+                break;
+        default:
+                for(;;){
+                        wm = wait();
+                        if(wm == nil)
+                                error("wait failed: %r", "");
+                        if(wm->pid == pid)
+                                break;
+                        free(wm);
+                }
+                if(debug)
+                        fprint(2, "wm->pid %d wm->msg == %s\n", wm->pid, wm->msg);
+
+                if(wm->msg[0]){
+                        if(debug)
+                                fprint(2, "[%d] wm->msg == %s\n", getpid(), wm->msg);
+                        if(!Rflag && strstr(wm->msg, "Retry")==0){
+                                /* return the message and remove it */
+                                if(returnmail(av, dp->name, wm->msg) != 0)
+                                        logit("returnmail failed", dp->name, av);
+                                remmatch(dp->name);
+                        } else {
+                                /* add sys to bad list and try again later */
+                                nbad++;
+                                badsys = realloc(badsys, nbad*sizeof(char*));
+                                badsys[nbad-1] = strdup(av[3]);
+                        }
+                } else {
+                        /* it worked remove the message */
+                        remmatch(dp->name);
+                }
+                free(wm);
+
+        }
+done:
+        if (l)
+                sysunlock(l);
+        Bterm(b);
+        sysunlockfile(Bfildes(b));
+        free(buf);
+        free(av);
+        close(dfd);
+}
+
+
+/*
+ *  return a name starting with the given character
+ */
+char*
+file(char *name, char type)
+{
+        static char nname[Elemlen+1];
+
+        strncpy(nname, name, Elemlen);
+        nname[Elemlen] = 0;
+        nname[0] = type;
+        return nname;
+}
+
+/*
+ *  send back the mail with an error message
+ *
+ *  return 0 if successful
+ */
+int
+returnmail(char **av, char *name, char *msg)
+{
+        int pfd[2];
+        Waitmsg *wm;
+        int fd;
+        char buf[256];
+        char attachment[256];
+        int i;
+        long n;
+        String *s;
+        char *sender;
+
+        if(av[1] == 0 || av[2] == 0){
+                logit("runq - dumping bad file", name, av);
+                return 0;
+        }
+
+        s = unescapespecial(s_copy(av[2]));
+        sender = s_to_c(s);
+
+        if(!returnable(sender) || strcmp(sender, "postmaster") == 0) {
+                logit("runq - dumping p to p mail", name, av);
+                return 0;
+        }
+
+        if(pipe(pfd) < 0){
+                logit("runq - pipe failed", name, av);
+                return -1;
+        }
+
+        switch(rfork(RFFDG|RFPROC|RFENVG)){
+        case -1:
+                logit("runq - fork failed", name, av);
+                return -1;
+        case 0:
+                logit("returning", name, av);
+                close(pfd[1]);
+                close(0);
+                dup(pfd[0], 0);
+                close(pfd[0]);
+                putenv("upasname", "/dev/null");
+                snprint(buf, sizeof(buf), "%s/marshal", UPASBIN);
+                snprint(attachment, sizeof(attachment), "%s", file(name, 'D'));
+                execl(buf, "send", "-A", attachment, "-s", "permanent failure", sender, nil);
+                error("can't exec", 0);
+                break;
+        default:
+                break;
+        }
+
+        close(pfd[0]);
+        fprint(pfd[1], "\n");        /* get out of headers */
+        if(av[1]){
+                fprint(pfd[1], "Your request ``%.20s ", av[1]);
+                for(n = 3; av[n]; n++)
+                        fprint(pfd[1], "%s ", av[n]);
+        }
+        fprint(pfd[1], "'' failed (code %s).\nThe symptom was:\n\n", msg);
+        fd = open(file(name, 'E'), OREAD);
+        if(fd >= 0){
+                for(;;){
+                        n = read(fd, buf, sizeof(buf));
+                        if(n <= 0)
+                                break;
+                        if(write(pfd[1], buf, n) != n){
+                                close(fd);
+                                goto out;
+                        }
+                }
+                close(fd);
+        }
+        close(pfd[1]);
+out:
+        wm = wait();
+        if(wm == nil){
+                syslog(0, "runq", "wait: %r");
+                logit("wait failed", name, av);
+                return -1;
+        }
+        i = 0;
+        if(wm->msg[0]){
+                i = -1;
+                syslog(0, "runq", "returnmail child: %s", wm->msg);
+                logit("returnmail child failed", name, av);
+        }
+        free(wm);
+        return i;
+}
+
+/*
+ *  print a warning and continue
+ */
+void
+warning(char *f, void *a)
+{
+        char err[65];
+        char buf[256];
+
+        rerrstr(err, sizeof(err));
+        snprint(buf, sizeof(buf), f, a);
+        fprint(2, "runq: %s: %s\n", buf, err);
+}
+
+/*
+ *  print an error and die
+ */
+void
+error(char *f, void *a)
+{
+        char err[Errlen];
+        char buf[256];
+
+        rerrstr(err, sizeof(err));
+        snprint(buf, sizeof(buf), f, a);
+        fprint(2, "runq: %s: %s\n", buf, err);
+        exits(buf);
+}
+
+void
+logit(char *msg, char *file, char **av)
+{
+        int n, m;
+        char buf[256];
+
+        n = snprint(buf, sizeof(buf), "%s/%s: %s", curdir, file, msg);
+        for(; *av; av++){
+                m = strlen(*av);
+                if(n + m + 4 > sizeof(buf))
+                        break;
+                sprint(buf + n, " '%s'", *av);
+                n += m + 3;
+        }
+        syslog(0, runqlog, "%s", buf);
+}
+
+char *loadfile = ".runqload";
+
+/*
+ *  load balancing
+ */
+void
+doload(int start)
+{
+        int fd;
+        char buf[32];
+        int i, n;
+        Mlock *l;
+        Dir *d;
+
+        if(load <= 0)
+                return;
+
+        if(chdir(root) < 0){
+                load = 0;
+                return;
+        }
+
+        l = syslock(loadfile);
+        fd = open(loadfile, ORDWR);
+        if(fd < 0){
+                fd = create(loadfile, 0666, ORDWR);
+                if(fd < 0){
+                        load = 0;
+                        sysunlock(l);
+                        return;
+                }
+        }
+
+        /* get current load */
+        i = 0;
+        n = read(fd, buf, sizeof(buf)-1);
+        if(n >= 0){
+                buf[n] = 0;
+                i = atoi(buf);
+        }
+        if(i < 0)
+                i = 0;
+
+        /* ignore load if file hasn't been changed in 30 minutes */
+        d = dirfstat(fd);
+        if(d != nil){
+                if(d->mtime + 30*60 < time(0))
+                        i = 0;
+                free(d);
+        }
+
+        /* if load already too high, give up */
+        if(start && i >= load){
+                sysunlock(l);
+                exits(0);
+        }
+
+        /* increment/decrement load */
+        if(start)
+                i++;
+        else
+                i--;
+        seek(fd, 0, 0);
+        fprint(fd, "%d\n", i);
+        sysunlock(l);
+        close(fd);
+}
diff --git a/src/cmd/upas/scanmail/common.c b/src/cmd/upas/scanmail/common.c
t@@ -0,0 +1,667 @@
+#include 
+#include 
+#include 
+#include 
+#include "spam.h"
+
+enum {
+        Quanta        = 8192,
+        Minbody = 6000,
+        HdrMax        = 15,
+};
+
+typedef struct keyword Keyword;
+typedef struct word Word;
+
+struct word{
+        char        *string;
+        int        n;
+};
+
+struct        keyword{
+        char        *string;
+        int        value;
+};
+
+Word        htmlcmds[] =
+{
+        "html",                4,
+        "!doctype html", 13,
+        0,
+
+};
+
+Word        hrefs[] =
+{
+        "a href=",        7,
+        "a title=",        8,
+        "a target=",        9,
+        "base href=",        10,
+        "img src=",        8,
+        "img border=",        11,
+        "form action=", 12,
+        "!--",                3,
+        0,
+
+};
+
+/*
+ *        RFC822 header keywords to look for for fractured header.
+ *        all lengths must be less than HdrMax defined above.
+ */
+Word        hdrwords[] =
+{
+        "cc:",                        3,
+        "bcc:",                 4,
+        "to:",                        3,
+        0,                        0,
+
+};
+
+Keyword        keywords[] =
+{
+        "header",        HoldHeader,
+        "line",                SaveLine,
+        "hold",                Hold,
+        "dump",                Dump,
+        "loff",                Lineoff,
+        0,                Nactions,
+};
+
+Patterns patterns[] = {
+[Dump]                { "DUMP:", 0, 0 },
+[HoldHeader]        { "HEADER:", 0, 0 },
+[Hold]                { "HOLD:", 0, 0 },
+[SaveLine]        { "LINE:", 0, 0 },
+[Lineoff]        { "LINEOFF:", 0, 0 },
+[Nactions]        { 0, 0, 0 },
+};
+
+static char*        endofhdr(char*, char*);
+static        int        escape(char**);
+static        int        extract(char*);
+static        int        findkey(char*);
+static        int        hash(int);
+static        int        isword(Word*, char*, int);
+static        void        parsealt(Biobuf*, char*, Spat**);
+
+/*
+ *        The canonicalizer: convert input to canonical representation
+ */
+char*
+readmsg(Biobuf *bp, int *hsize, int *bufsize)
+{
+        char *p, *buf;
+        int n, offset, eoh, bsize, delta;
+
+        buf = 0;
+        offset = 0;
+        if(bufsize)
+                *bufsize = 0;
+        if(hsize)
+                *hsize = 0;
+        for(;;) {
+                buf = Realloc(buf, offset+Quanta+1);
+                n = Bread(bp, buf+offset, Quanta);
+                if(n < 0){
+                        free(buf);
+                        return 0;
+                }
+                p = buf+offset;                        /* start of this chunk */
+                offset += n;                        /* end of this chunk */
+                buf[offset] = 0;
+                if(n == 0){
+                        if(offset == 0)
+                                return 0;
+                        break;
+                }
+
+                if(hsize == 0)                        /* don't process header */
+                        break;
+                if(p != buf && p[-1] == '\n')        /* check for EOH across buffer split */
+                        p--;
+                p = endofhdr(p, buf+offset);
+                if(p)
+                        break;
+                if(offset >= Maxread)                /* gargantuan header - just punt*/
+                {
+                        if(hsize)
+                                *hsize = offset;
+                        if(bufsize)
+                                *bufsize = offset;
+                        return buf;
+                }
+        }
+        eoh = p-buf;                                /* End of header */
+        bsize = offset - eoh;                        /* amount of body already read */
+
+                /* Read at least Minbody bytes of the body */
+        if (bsize < Minbody){
+                delta = Minbody-bsize;
+                buf = Realloc(buf, offset+delta+1);
+                n = Bread(bp, buf+offset, delta);
+                if(n > 0) {
+                        offset += n;
+                        buf[offset] = 0;
+                }
+        }
+        if(hsize)
+                *hsize = eoh;
+        if(bufsize)
+                *bufsize = offset;
+        return buf;
+}
+
+static        int
+isword(Word *wp, char *text, int len)
+{
+        for(;wp->string; wp++)
+                if(len >= wp->n && strncmp(text, wp->string, wp->n) == 0)
+                        return 1;
+        return 0;
+}
+
+static char*
+endofhdr(char *raw, char *end)
+{
+        int i;
+        char *p, *q;
+        char buf[HdrMax];
+
+        /*
+          * can't use strchr to search for newlines because
+         * there may be embedded NULL's.
+         */
+        for(p = raw; p < end; p++){
+                if(*p != '\n' || p[1] != '\n')
+                        continue;
+                p++;
+                for(i = 0, q = p+1; i < sizeof(buf) && *q; q++){
+                        buf[i++] = tolower(*q);
+                        if(*q == ':' || *q == '\n')
+                                break;
+                }
+                if(!isword(hdrwords, buf, i))
+                        return p+1;
+        }
+        return 0;
+}
+
+static        int
+htmlmatch(Word *wp, char *text, char *end, int *n)
+{
+        char *cp;
+        int i, c, lastc;
+        char buf[MaxHtml];
+
+        /*
+         * extract a string up to '>'
+         */
+
+        i = lastc = 0;
+        cp = text;
+        while (cp < end && i < sizeof(buf)-1){
+                c = *cp++;
+                if(c == '=')
+                        c = escape(&cp);
+                switch(c){
+                case 0:
+                case '\r':
+                        continue;
+                case '>':
+                        goto out;
+                case '\n':
+                case ' ':
+                case '\t':
+                        if(lastc == ' ')
+                                continue;
+                        c = ' ';
+                        break;
+                default:
+                        c = tolower(c);
+                        break;
+                }
+                buf[i++] = lastc = c;
+        }
+out:
+        buf[i] = 0;
+        if(n)
+                *n = cp-text;
+        return isword(wp, buf, i);
+}
+
+static int
+escape(char **msg)
+{
+        int c;
+        char *p;
+
+        p = *msg;
+        c = *p;
+        if(c == '\n'){
+                p++;
+                c = *p++;
+        } else
+        if(c == '2'){
+                c = tolower(p[1]);
+                if(c == 'e'){
+                        p += 2;
+                        c = '.';
+                }else
+                if(c == 'f'){
+                        p += 2;
+                        c = '/';
+                }else
+                if(c == '0'){
+                        p += 2;
+                        c = ' ';
+                }
+                else c = '=';
+        } else {
+                if(c == '3' && tolower(p[1]) == 'd')
+                        p += 2;
+                c = '=';
+        }
+        *msg = p;
+        return c;
+}
+
+static int
+htmlchk(char **msg, char *end)
+{
+        int n;
+        char *p;
+
+        static int ishtml;
+
+        p = *msg;
+        if(ishtml == 0){
+                ishtml = htmlmatch(htmlcmds, p, end, &n);
+        
+                /* If not an HTML keyword, check if it's
+                 * an HTML comment ().  if so,
+                 * skip over it; otherwise copy it in.
+                 */
+                if(ishtml == 0 && *p != '!')        /* not comment */
+                        return '<';                /* copy it */
+
+        } else if(htmlmatch(hrefs, p, end, &n))        /* if special HTML string  */
+                return '<';                        /* copy it */
+        
+        /*
+         * this is an uninteresting HTML command; skip over it.
+         */
+        p += n;
+        *msg = p+1;
+        return *p;
+}
+
+/*
+ * decode a base 64 encode body
+ */
+void
+conv64(char *msg, char *end, char *buf, int bufsize)
+{
+        int len, i;
+        char *cp;
+
+        len = end - msg;
+        i = (len*3)/4+1;        // room for max chars + null
+        cp = Malloc(i);
+        len = dec64((uchar*)cp, i, msg, len);
+        convert(cp, cp+len, buf, bufsize, 1);
+        free(cp);
+}
+
+int
+convert(char *msg, char *end, char *buf, int bufsize, int isbody)
+{
+
+        char *p;
+        int c, lastc, base64;
+
+        lastc = 0;
+        base64 = 0;
+        while(msg < end && bufsize > 0){
+                c = *msg++;
+
+                /*
+                 * In the body only, try to strip most HTML and
+                 * replace certain MIME escape sequences with the character
+                 */
+                if(isbody) {
+                        do{
+                                p = msg;
+                                if(c == '<')
+                                        c = htmlchk(&msg, end);
+                                if(c == '=')
+                                        c = escape(&msg);
+                        } while(p != msg && p < end);
+                }
+                switch(c){
+                case 0:
+                case '\r':
+                        continue;
+                case '\t':
+                case ' ':
+                case '\n':
+                        if(lastc == ' ')
+                                continue;
+                        c = ' ';
+                        break;
+                case 'C':        /* check for MIME base 64 encoding in header */
+                case 'c':
+                        if(isbody == 0)
+                        if(msg < end-32 && *msg == 'o' && msg[1] == 'n')
+                        if(cistrncmp(msg+2, "tent-transfer-encoding: base64", 30) == 0)
+                                base64 = 1;
+                        c = 'c';
+                        break;
+                default:
+                        c = tolower(c);
+                        break;
+                }
+                *buf++ = c;
+                lastc = c;
+                bufsize--;
+        }
+        *buf = 0;
+        return base64;
+}
+
+/*
+ *        The pattern parser: build data structures from the pattern file
+ */
+
+static int
+hash(int c)
+{
+        return c & 127;
+}
+
+static        int
+findkey(char *val)
+{
+        Keyword *kp;
+
+        for(kp = keywords; kp->string; kp++)
+                if(strcmp(val, kp->string) == 0)
+                                break;
+        return kp->value;
+}
+
+#define        whitespace(c)        ((c) == ' ' || (c) == '\t')
+
+void
+parsepats(Biobuf *bp)
+{
+        Pattern *p, *new;
+        char *cp, *qp;
+        int type, action, n, h;
+        Spat *spat;
+
+        for(;;){
+                cp = Brdline(bp, '\n');
+                if(cp == 0)
+                        break;
+                cp[Blinelen(bp)-1] = 0;
+                while(*cp == ' ' || *cp == '\t')
+                        cp++;
+                if(*cp == '#' || *cp == 0)
+                        continue;
+                type = regexp;
+                if(*cp == '*'){
+                        type = string;
+                        cp++;
+                }
+                qp = strchr(cp, ':');
+                if(qp == 0)
+                        continue;
+                *qp = 0;
+                if(debug)
+                        fprint(2, "action = %s\n", cp);
+                action = findkey(cp);
+                if(action >= Nactions)
+                        continue;
+                cp = qp+1;
+                n = extract(cp);
+                if(n <= 0 || *cp == 0)
+                        continue;
+
+                qp = strstr(cp, "~~");
+                if(qp){
+                        *qp = 0;
+                        n = strlen(cp);
+                }
+                if(debug)
+                        fprint(2, " Pattern: `%s'\n", cp);
+
+                        /* Hook regexps into a chain */
+                if(type == regexp) {
+                        new = Malloc(sizeof(Pattern));
+                        new->action = action;
+                        new->pat = regcomp(cp);
+                        if(new->pat == 0){
+                                free(new);
+                                continue;
+                        }
+                        new->type = regexp;
+                        new->alt = 0;
+                        new->next = 0;
+
+                        if(qp)
+                                parsealt(bp, qp+2, &new->alt);
+
+                        new->next = patterns[action].regexps;
+                        patterns[action].regexps = new;
+                        continue;
+
+                }
+                        /* not a Regexp - hook strings into Pattern hash chain */
+                spat = Malloc(sizeof(*spat));
+                spat->next = 0;
+                spat->alt = 0;
+                spat->len = n;
+                spat->string = Malloc(n+1);
+                spat->c1 = cp[1];
+                strcpy(spat->string, cp);
+
+                if(qp)
+                        parsealt(bp, qp+2, &spat->alt);
+
+                p = patterns[action].strings;
+                if(p == 0) {
+                        p = Malloc(sizeof(Pattern));
+                        memset(p, 0, sizeof(*p));
+                        p->action = action;
+                        p->type = string;
+                        patterns[action].strings = p;
+                }
+                h = hash(*spat->string);
+                spat->next = p->spat[h];
+                p->spat[h] = spat;
+        }
+}
+
+static void
+parsealt(Biobuf *bp, char *cp, Spat** head)
+{
+        char *p;
+        Spat *alt;
+
+        while(cp){
+                if(*cp == 0){                /*escaped newline*/
+                        do{
+                                cp = Brdline(bp, '\n');
+                                if(cp == 0)
+                                        return;
+                                cp[Blinelen(bp)-1] = 0;
+                        } while(extract(cp) <= 0 || *cp == 0);
+                }
+
+                p = cp;
+                cp = strstr(p, "~~");
+                if(cp){
+                        *cp = 0;
+                        cp += 2;
+                }
+                if(strlen(p)){
+                        alt = Malloc(sizeof(*alt));
+                        alt->string = strdup(p);
+                        alt->next = *head;
+                        *head = alt;
+                }
+        }
+}
+
+static int
+extract(char *cp)
+{
+        int c;
+        char *p, *q, *r;
+
+        p = q = r = cp;
+        while(whitespace(*p))
+                p++;
+        while(c = *p++){
+                if (c == '#')
+                        break;
+                if(c == '"'){
+                        while(*p && *p != '"'){
+                                if(*p == '\\' && p[1] == '"')
+                                        p++;
+                                if('A' <= *p && *p <= 'Z')
+                                        *q++ = *p++ + ('a'-'A');
+                                else
+                                        *q++ = *p++;
+                        }
+                        if(*p)
+                                p++;
+                        r = q;                /* never back up over a quoted string */
+                } else {
+                        if('A' <= c && c <= 'Z')
+                                c += ('a'-'A');
+                        *q++ = c;
+                }
+        }
+        while(q > r && whitespace(q[-1]))
+                q--;
+        *q = 0;
+        return q-cp;
+}
+
+/*
+ *        The matching engine: compare canonical input to pattern structures
+ */
+
+static Spat*
+isalt(char *message, Spat *alt)
+{
+        while(alt) {
+                if(*cmd)
+                if(message != cmd && strstr(cmd, alt->string))
+                        break;
+                if(message != header+1 && strstr(header+1, alt->string))
+                        break;
+                if(strstr(message, alt->string))
+                        break;
+                alt = alt->next;
+        }
+        return alt;
+}
+
+int
+matchpat(Pattern *p, char *message, Resub *m)
+{
+        Spat *spat;
+        char *s;
+        int c, c1;
+
+        if(p->type == string){
+                c1 = *message;
+                for(s=message; c=c1; s++){
+                        c1 = s[1];
+                        for(spat=p->spat[hash(c)]; spat; spat=spat->next){
+                                if(c1 == spat->c1)
+                                if(memcmp(s, spat->string, spat->len) == 0)
+                                if(!isalt(message, spat->alt)){
+                                        m->sp = s;
+                                        m->ep = s + spat->len;
+                                        return 1;
+                                }
+                        }
+                }
+                return 0;
+        }
+        m->sp = m->ep = 0;
+        if(regexec(p->pat, message, m, 1) == 0)
+                return 0;
+        if(isalt(message, p->alt))
+                return 0;
+        return 1;
+}
+
+
+void
+xprint(int fd, char *type, Resub *m)
+{
+        char *p, *q;
+        int i;
+
+        if(m->sp == 0 || m->ep == 0)
+                return;
+
+                /* back up approx 30 characters to whitespace */
+        for(p = m->sp, i = 0; *p && i < 30; i++, p--)
+                        ;
+        while(*p && *p != ' ')
+                p--;
+        p++;
+
+                /* grab about 30 more chars beyond the end of the match */
+        for(q = m->ep, i = 0; *q && i < 30; i++, q++)
+                        ;
+        while(*q && *q != ' ')
+                q++;
+
+        fprint(fd, "%s %.*s~%.*s~%.*s\n", type, (int)(m->sp-p), p, (int)(m->ep-m->sp), m->sp, (int)(q-m->ep), m->ep);
+}
+
+enum {
+        INVAL=        255
+};
+
+static uchar t64d[256] = {
+/*00 */        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*10*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*20*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL,    62, INVAL, INVAL, INVAL,    63,
+/*30*/           52,          53,         54,        55,    56,    57,    58,    59,
+           60,          61, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*40*/        INVAL,    0,      1,     2,     3,     4,     5,     6,
+            7,    8,      9,    10,    11,    12,    13,    14,
+/*50*/           15,   16,     17,    18,    19,    20,    21,    22,
+           23,   24,     25, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*60*/        INVAL,   26,     27,    28,    29,    30,    31,    32,
+           33,   34,     35,    36,    37,    38,    39,    40,
+/*70*/           41,   42,     43,    44,    45,    46,    47,    48,
+           49,   50,     51, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*80*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*90*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*A0*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*B0*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*C0*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*D0*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*E0*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+/*F0*/        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+        INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL, INVAL,
+};
diff --git a/src/cmd/upas/scanmail/mkfile b/src/cmd/upas/scanmail/mkfile
t@@ -0,0 +1,24 @@
+
diff --git a/src/cmd/upas/scanmail/scanmail.c b/src/cmd/upas/scanmail/scanmail.c
t@@ -0,0 +1,476 @@
+#include "common.h"
+#include "spam.h"
+
+int        cflag;
+int        debug;
+int        hflag;
+int        nflag;
+int        sflag;
+int        tflag;
+int        vflag;
+Biobuf        bin, bout, *cout;
+
+        /* file names */
+char        patfile[128];
+char        linefile[128];
+char        holdqueue[128];
+char        copydir[128];
+
+char        header[Hdrsize+2];
+char        cmd[1024];
+char        **qname;
+char        **qdir;
+char        *sender;
+String        *recips;
+
+char*        canon(Biobuf*, char*, char*, int*);
+int        matcher(char*, Pattern*, char*, Resub*);
+int        matchaction(int, char*, Resub*);
+Biobuf        *opencopy(char*);
+Biobuf        *opendump(char*);
+char        *qmail(char**, char*, int, Biobuf*);
+void        saveline(char*, char*, Resub*);
+int        optoutofspamfilter(char*);
+
+void
+usage(void)
+{
+        fprint(2, "missing or bad arguments to qer\n");
+        exits("usage");
+}
+
+void
+regerror(char *s)
+{
+        fprint(2, "scanmail: %s\n", s);
+}
+
+void *
+Malloc(long n)
+{
+        void *p;
+
+        p = malloc(n);
+        if(p == 0)
+                exits("malloc");
+        return p;
+}
+
+void*
+Realloc(void *p, ulong n)
+{
+        p = realloc(p, n);
+        if(p == 0)
+                exits("realloc");
+        return p;
+}
+
+void
+main(int argc, char *argv[])
+{
+        int i, n, nolines, optout;
+        char **args, **a, *cp, *buf;
+        char body[Bodysize+2];
+        Resub match[1];
+        Biobuf *bp;
+
+        optout = 1;
+        a = args = Malloc((argc+1)*sizeof(char*));
+        sprint(patfile, "%s/patterns", UPASLIB);
+        sprint(linefile, "%s/lines", UPASLOG);
+        sprint(holdqueue, "%s/queue.hold", SPOOL);
+        sprint(copydir, "%s/copy", SPOOL);
+
+        *a++ = argv[0];
+        for(argc--, argv++; argv[0] && argv[0][0] == '-'; argc--, argv++){
+                switch(argv[0][1]){
+                case 'c':                        /* save copy of message */
+                        cflag = 1;
+                        break;
+                case 'd':                        /* debug */
+                        debug++;
+                        *a++ = argv[0];
+                        break;
+                case 'h':                        /* queue held messages by sender domain */
+                        hflag = 1;                /* -q flag must be set also */
+                        break;
+                case 'n':                        /* NOHOLD mode */
+                        nflag = 1;
+                        break;
+                case 'p':                        /* pattern file */
+                        if(argv[0][2] || argv[1] == 0)
+                                usage();
+                        argc--;
+                        argv++;
+                        strecpy(patfile, patfile+sizeof patfile, *argv);
+                        break;
+                case 'q':                        /* queue name */
+                        if(argv[0][2] ||  argv[1] == 0)
+                                usage();
+                        *a++ = argv[0];
+                        argc--;
+                        argv++;
+                        qname = a;
+                        *a++ = argv[0];
+                        break;
+                case 's':                        /* save copy of dumped message */
+                        sflag = 1;
+                        break;
+                case 't':                        /* test mode - don't log match
+                                                 * and write message to /dev/null
+                                                 */
+                        tflag = 1;
+                        break;
+                case 'v':                        /* vebose - print matches */
+                        vflag = 1;
+                        break;
+                default:
+                        *a++ = argv[0];
+                        break;
+                }
+        }
+
+        if(argc < 3)
+                usage();
+
+        Binit(&bin, 0, OREAD);
+        bp = Bopen(patfile, OREAD);
+        if(bp){
+                parsepats(bp);
+                Bterm(bp);
+        }
+        qdir = a;
+        sender = argv[2];
+
+                /* copy the rest of argv, acummulating the recipients as we go */
+        for(i = 0; argv[i]; i++){
+                *a++ = argv[i];
+                if(i < 4)        /* skip queue, 'mail', sender, dest sys */
+                        continue;
+                        /* recipients and smtp flags - skip the latter*/
+                if(strcmp(argv[i], "-g") == 0){
+                        *a++ = argv[++i];
+                        continue;
+                }
+                if(recips)
+                        s_append(recips, ", ");
+                else
+                        recips = s_new();
+                s_append(recips, argv[i]);
+                if(optout && !optoutofspamfilter(argv[i]))
+                        optout = 0;
+        }
+        *a = 0;
+                /* construct a command string for matching */
+        snprint(cmd, sizeof(cmd)-1, "%s %s", sender, s_to_c(recips));
+        cmd[sizeof(cmd)-1] = 0;
+        for(cp = cmd; *cp; cp++)
+                *cp = tolower(*cp);
+
+                /* canonicalize a copy of the header and body.
+                 * buf points to orginal message and n contains
+                 * number of bytes of original message read during
+                 * canonicalization.
+                 */
+        *body = 0;
+        *header = 0;
+        buf = canon(&bin, header+1, body+1, &n);
+        if (buf == 0)
+                exits("read");
+
+                /* if all users opt out, don't try matches */
+        if(optout){
+                if(cflag)
+                        cout = opencopy(sender);
+                exits(qmail(args, buf, n, cout));
+        }
+
+                /* Turn off line logging, if command line matches */
+        nolines = matchaction(Lineoff, cmd, match);
+
+        for(i = 0; patterns[i].action; i++){
+                        /* Lineoff patterns were already done above */
+                if(i == Lineoff)
+                        continue;
+                        /* don't apply "Line" patterns if excluded above */
+                if(nolines && i == SaveLine)
+                        continue;
+                        /* apply patterns to the sender/recips, header and body */
+                if(matchaction(i, cmd, match))
+                        break;
+                if(matchaction(i, header+1, match))
+                        break;
+                if(i == HoldHeader)
+                        continue;
+                if(matchaction(i, body+1, match))
+                        break;
+        }
+        if(cflag && patterns[i].action == 0)        /* no match found - save msg */
+                cout = opencopy(sender);
+
+        exits(qmail(args, buf, n, cout));
+}
+
+char*
+qmail(char **argv, char *buf, int n, Biobuf *cout)
+{
+        Waitmsg *status;
+        int i, pid, pipefd[2];
+        char path[512];
+        Biobuf *bp;
+
+        pid = 0;
+        if(tflag == 0){
+                if(pipe(pipefd) < 0)
+                        exits("pipe");
+                pid = fork();
+                if(pid == 0){
+                        dup(pipefd[0], 0);
+                        for(i = sysfiles(); i >= 3; i--)
+                                close(i);
+                        snprint(path, sizeof(path), "%s/qer", UPASBIN);
+                        *argv=path;
+                        exec(path, argv);
+                        exits("exec");
+                }
+                Binit(&bout, pipefd[1], OWRITE);
+                bp = &bout;
+        } else
+                bp = Bopen("/dev/null", OWRITE);
+
+        while(n > 0){
+                Bwrite(bp, buf, n);
+                if(cout)
+                        Bwrite(cout, buf, n);
+                n = Bread(&bin, buf, sizeof(buf)-1);
+        }
+        Bterm(bp);
+        if(cout)
+                Bterm(cout);
+        if(tflag)
+                return 0;
+
+        close(pipefd[1]);
+        close(pipefd[0]);
+        for(;;){
+                status = wait();
+                if(status == nil || status->pid == pid)
+                        break;
+                free(status);
+        }
+        if(status == nil)
+                strcpy(buf, "wait failed");
+        else{
+                strcpy(buf, status->msg);
+                free(status);
+        }
+        return buf;
+}
+
+char*
+canon(Biobuf *bp, char *header, char *body, int *n)
+{
+        int hsize;
+        char *raw;
+
+        hsize = 0;
+        *header = 0;
+        *body = 0;
+        raw = readmsg(bp, &hsize, n);
+        if(raw){
+                if(convert(raw, raw+hsize, header, Hdrsize, 0))
+                        conv64(raw+hsize, raw+*n, body, Bodysize);        /* base64 */
+                else
+                        convert(raw+hsize, raw+*n, body, Bodysize, 1);        /* text */
+        }
+        return raw;
+}
+
+int
+matchaction(int action, char *message, Resub *m)
+{
+        char *name;
+        Pattern *p;
+
+        if(message == 0 || *message == 0)
+                return 0;
+
+        name = patterns[action].action;
+        p = patterns[action].strings;
+        if(p)
+                if(matcher(name, p, message, m))
+                        return 1;
+
+        for(p = patterns[action].regexps; p; p = p->next)
+                if(matcher(name, p, message, m))
+                        return 1;
+        return 0;
+}
+
+int
+matcher(char *action, Pattern *p, char *message, Resub *m)
+{
+        char *cp;
+        String *s;
+
+        for(cp = message; matchpat(p, cp, m); cp = m->ep){
+                switch(p->action){
+                case SaveLine:
+                        if(vflag)
+                                xprint(2, action, m);
+                        saveline(linefile, sender, m);
+                        break;
+                case HoldHeader:
+                case Hold:
+                        if(nflag)
+                                continue;
+                        if(vflag)
+                                xprint(2, action, m);
+                        *qdir = holdqueue;
+                        if(hflag && qname){
+                                cp = strchr(sender, '!');
+                                if(cp){
+                                        *cp = 0;
+                                        *qname = strdup(sender);
+                                        *cp = '!';
+                                } else
+                                        *qname = strdup(sender);
+                        }
+                        return 1;
+                case Dump:
+                        if(vflag)
+                                xprint(2, action, m);
+                        *(m->ep) = 0;
+                        if(!tflag){
+                                s = s_new();
+                                s_append(s, sender);
+                                s = unescapespecial(s);
+                                syslog(0, "smtpd", "Dumped %s [%s] to %s", s_to_c(s), m->sp,
+                                        s_to_c(s_restart(recips)));
+                                s_free(s);
+                        }
+                        tflag = 1;
+                        if(sflag)
+                                cout = opendump(sender);
+                        return 1;
+                default:
+                        break;
+                }
+        }
+        return 0;
+}
+
+void
+saveline(char *file, char *sender, Resub *rp)
+{
+        char *p, *q;
+        int i, c;
+        Biobuf *bp;
+
+        if(rp->sp == 0 || rp->ep == 0)
+                return;
+                /* back up approx 20 characters to whitespace */
+        for(p = rp->sp, i = 0; *p && i < 20; i++, p--)
+                        ;
+        while(*p && *p != ' ')
+                p--;
+        p++;
+
+                /* grab about 20 more chars beyond the end of the match */
+        for(q = rp->ep, i = 0; *q && i < 20; i++, q++)
+                        ;
+        while(*q && *q != ' ')
+                q++;
+
+        c = *q;
+        *q = 0;
+        bp = sysopen(file, "al", 0644);
+        if(bp){
+                Bprint(bp, "%s-> %s\n", sender, p);
+                Bterm(bp);
+        }
+        else if(debug)
+                fprint(2, "can't save line: (%s) %s\n", sender, p);
+        *q = c;
+}
+
+Biobuf*
+opendump(char *sender)
+{
+        int i;
+        ulong h;
+        char buf[512];
+        Biobuf *b;
+        char *cp;
+
+        cp = ctime(time(0));
+        cp[7] = 0;
+        cp[10] = 0;
+        if(cp[8] == ' ')
+                sprint(buf, "%s/queue.dump/%s%c", SPOOL, cp+4, cp[9]);
+        else
+                sprint(buf, "%s/queue.dump/%s%c%c", SPOOL, cp+4, cp[8], cp[9]);
+        cp = buf+strlen(buf);
+        if(access(buf, 0) < 0 && sysmkdir(buf, 0777) < 0){
+                syslog(0, "smtpd", "couldn't dump mail from %s: %r", sender);
+                return 0;
+        }
+
+        h = 0;
+        while(*sender)
+                h = h*257 + *sender++;
+        for(i = 0; i < 50; i++){
+                h += lrand();
+                sprint(cp, "/%lud", h);
+                b = sysopen(buf, "wlc", 0644);
+                if(b){
+                        if(vflag)
+                                fprint(2, "saving in %s\n", buf);
+                        return b;
+                }
+        }
+        return 0;
+}
+
+Biobuf*
+opencopy(char *sender)
+{
+        int i;
+        ulong h;
+        char buf[512];
+        Biobuf *b;
+
+        h = 0;
+        while(*sender)
+                h = h*257 + *sender++;
+        for(i = 0; i < 50; i++){
+                h += lrand();
+                sprint(buf, "%s/%lud", copydir, h);
+                b = sysopen(buf, "wlc", 0600);
+                if(b)
+                        return b;
+        }
+        return 0;
+}
+
+int
+optoutofspamfilter(char *addr)
+{
+        char *p, *f;
+        int rv;
+
+        p = strchr(addr, '!');
+        if(p)
+                p++;
+        else
+                p = addr;
+
+        rv = 0;
+        f = smprint("/mail/box/%s/nospamfiltering", p);
+        if(f != nil){
+                rv = access(f, 0)==0;
+                free(f);
+        }
+
+        return rv;
+}
diff --git a/src/cmd/upas/scanmail/spam.h b/src/cmd/upas/scanmail/spam.h
t@@ -0,0 +1,62 @@
+
+enum{
+        Dump                = 0,                /* Actions must be in order of descending importance */
+        HoldHeader,
+        Hold,
+        SaveLine,
+        Lineoff,                        /* Lineoff must be the last action code */
+        Nactions,
+
+        Nhash                = 128,
+
+        regexp                = 1,                /* types: literal string or regular expression */
+        string                = 2,
+
+        MaxHtml                = 256,
+        Hdrsize                = 4096,
+        Bodysize        = 8192,
+        Maxread                = 64*1024,
+};
+
+typedef struct spat         Spat;
+typedef struct pattern        Pattern;
+typedef        struct patterns        Patterns;
+struct        spat
+{
+        char*        string;
+        int        len;
+        int        c1;
+        Spat*        next;
+        Spat*        alt;
+};
+
+struct        pattern{
+        struct        pattern *next;
+        int        action;
+        int        type;
+        Spat*        alt;
+        union{
+                Reprog*        pat;
+                Spat*        spat[Nhash];
+        };
+};
+
+struct        patterns {
+        char        *action;
+        Pattern        *strings;
+        Pattern        *regexps;
+};
+
+extern        int        debug;
+extern        Patterns patterns[];
+extern        char        header[];
+extern        char        cmd[];
+
+extern        void        conv64(char*, char*, char*, int);
+extern        int        convert(char*, char*, char*, int, int);
+extern        void*        Malloc(long n);
+extern        int        matchpat(Pattern*, char*, Resub*);
+extern        char*        readmsg(Biobuf*, int*, int*);
+extern        void        parsepats(Biobuf*);
+extern        void*        Realloc(void*, ulong);
+extern        void        xprint(int, char*, Resub*);
diff --git a/src/cmd/upas/scanmail/testscan.c b/src/cmd/upas/scanmail/testscan.c
t@@ -0,0 +1,212 @@
+#include "sys.h"
+#include "spam.h"
+
+int         debug;
+Biobuf        bin;
+char        patfile[128], header[Hdrsize+2];
+char        cmd[1024];
+
+char*        canon(Biobuf*, char*, char*, int*);
+int        matcher(char *, Pattern*, char*, Resub*);
+int        matchaction(Patterns*, char*);
+
+void
+usage(void)
+{
+        fprint(2, "missing or bad arguments to qer\n");
+        exits("usage");
+}
+
+void *
+Malloc(long n)
+{
+        void *p;
+
+        p = malloc(n);
+        if(p == 0){
+                fprint(2, "malloc error");
+                exits("malloc");
+        }
+        return p;
+}
+
+void*
+Realloc(void *p, ulong n)
+{
+        p = realloc(p, n);
+        if(p == 0){
+                fprint(2, "realloc error");
+                exits("realloc");
+        }
+        return p;
+}
+
+void
+dumppats(void)
+{
+        int i, j;
+        Pattern *p;
+        Spat *s, *q;
+
+        for(i = 0; patterns[i].action; i++){
+                for(p = patterns[i].regexps; p; p = p->next){
+                        print("%s \n", patterns[i].action);
+                        if(p->alt)
+                                print("Alt:");
+                        for(s = p->alt; s; s = s->next)
+                                print("\t%s\n", s->string);
+                }
+                p = patterns[i].strings;
+                if(p == 0)
+                        continue;
+
+                for(j = 0; j < Nhash; j++){
+                        for(s = p->spat[j]; s; s = s->next){
+                                print("%s %s\n", patterns[i].action, s->string);
+                                if(s->alt)
+                                        print("Alt:");
+                                for(q = s->alt; q; q = q->next)
+                                        print("\t%s\n", q->string);
+                        }
+                }
+        }
+}
+
+void
+main(int argc, char *argv[])
+{
+        int i, fd, n, aflag, vflag;
+        char body[Bodysize+2], *raw, *ret;
+        Biobuf *bp;
+
+        sprint(patfile, "%s/patterns", UPASLIB);
+        aflag = -1;
+        vflag = 0;
+        ARGBEGIN {
+        case 'a':
+                aflag = 1;
+                break;
+        case 'v':
+                vflag = 1;
+                break;
+        case 'd':
+                debug++;
+                break;
+        case 'p':
+                strcpy(patfile,ARGF());
+                break;
+        } ARGEND
+
+        bp = Bopen(patfile, OREAD);
+        if(bp){
+                parsepats(bp);
+                Bterm(bp);
+        }
+
+        if(argc >= 1){
+                fd = open(*argv, OREAD);
+                if(fd < 0){
+                        fprint(2, "can't open %s\n", *argv);
+                        exits("open");
+                }
+                Binit(&bin, fd, OREAD);
+        } else 
+                Binit(&bin, 0, OREAD);
+
+        *body = 0;
+        *header = 0;
+        ret = 0;
+        for(;;){
+                raw = canon(&bin, header+1, body+1, &n);
+                if(raw == 0)
+                        break;
+                if(aflag == 0)
+                        continue;
+                if(aflag < 0)
+                        aflag = 0;
+                if(vflag){
+                        if(header[1]) {
+                                fprint(2, "\t**** Header ****\n\n");
+                                write(2, header+1, strlen(header+1));
+                                fprint(2, "\n");
+                        }
+                        fprint(2, "\t**** Body ****\n\n");
+                        if(body[1])
+                                write(2, body+1, strlen(body+1));
+                        fprint(2, "\n");
+                }
+
+                for(i = 0; patterns[i].action; i++){
+                        if(matchaction(&patterns[i], header+1))
+                                ret = patterns[i].action;
+                        if(i == HoldHeader)
+                                continue;
+                        if(matchaction(&patterns[i], body+1))
+                                ret = patterns[i].action;
+                }
+        }
+        exits(ret);
+}
+
+char*
+canon(Biobuf *bp, char *header, char *body, int *n)
+{
+        int hsize, base64;
+
+        static char *raw;
+
+        hsize = 0;
+        base64 = 0;
+        *header = 0;
+        *body = 0;
+        if(raw == 0){
+                raw = readmsg(bp, &hsize, n);
+                if(raw)
+                        base64 = convert(raw, raw+hsize, header, Hdrsize, 0);
+        } else {
+                free(raw);
+                raw = readmsg(bp, 0, n);
+        }
+        if(raw){
+                if(base64)
+                        conv64(raw+hsize, raw+*n, body, Bodysize);
+                else
+                        convert(raw+hsize, raw+*n, body, Bodysize, 1);
+        }
+        return raw;
+}
+
+int
+matchaction(Patterns *pp, char *message)
+{
+        char *name, *cp;
+        int ret;
+        Pattern *p;
+        Resub m[1];
+
+        if(message == 0 || *message == 0)
+                return 0;
+
+        name = pp->action;
+        p = pp->strings;
+        ret = 0;
+        if(p)
+                for(cp = message; matcher(name, p, cp, m); cp = m[0].ep)
+                                ret++;
+
+        for(p = pp->regexps; p; p = p->next)
+                for(cp = message; matcher(name, p, cp, m); cp = m[0].ep)
+                                ret++;
+        return ret;
+}
+
+int
+matcher(char *action, Pattern *p, char *message, Resub *m)
+{
+        if(matchpat(p, message, m)){
+                if(p->action != Lineoff)
+                        xprint(1, action, m);
+                return 1;
+        }
+        return 0;
+}
diff --git a/src/cmd/upas/send/authorize.c b/src/cmd/upas/send/authorize.c
t@@ -0,0 +1,29 @@
+#include "common.h"
+#include "send.h"
+
+/*
+ *  Run a command to authorize or refuse entry.  Return status 0 means
+ *  authorize, -1 means refuse.
+ */
+void
+authorize(dest *dp)
+{
+        process *pp;
+        String *errstr;
+
+        dp->authorized = 1;
+        pp = proc_start(s_to_c(dp->repl1), (stream *)0, (stream *)0, outstream(), 1, 0);
+        if (pp == 0){
+                dp->status = d_noforward;
+                return;
+        }
+        errstr = s_new();
+        while(s_read_line(pp->std[2]->fp, errstr))
+                ;
+        if ((dp->pstat = proc_wait(pp)) != 0) {
+                dp->repl2 = errstr;
+                dp->status = d_noforward;
+        } else
+                s_free(errstr);
+        proc_free(pp);
+}
diff --git a/src/cmd/upas/send/bind.c b/src/cmd/upas/send/bind.c
t@@ -0,0 +1,133 @@
+#include "common.h"
+#include "send.h"
+
+static int forward_loop(char *, char *);
+
+/* bind the destinations to the commands to be executed */
+extern dest *
+up_bind(dest *destp, message *mp, int checkforward)
+{
+        dest *list[2];                /* lists of unbound destinations */
+        int li;                        /* index into list[2] */
+        dest *bound=0;        /* bound destinations */
+        dest *dp;
+        int i;
+
+        list[0] = destp;
+        list[1] = 0;
+
+        /*
+         *  loop once to check for:
+         *        - forwarding rights
+         *        - addressing loops
+         *        - illegal characters
+         *        - characters that need escaping
+         */
+        for (dp = d_rm(&list[0]); dp != 0; dp = d_rm(&list[0])) {
+                if (!checkforward)
+                        dp->authorized = 1;
+                dp->addr = escapespecial(dp->addr);
+                if (forward_loop(s_to_c(dp->addr), thissys)) {
+                        dp->status = d_eloop;
+                        d_same_insert(&bound, dp);
+                } else if(forward_loop(s_to_c(mp->sender), thissys)) {
+                        dp->status = d_eloop;
+                        d_same_insert(&bound, dp);
+                } else if(shellchars(s_to_c(dp->addr))) {
+                        dp->status = d_syntax;
+                        d_same_insert(&bound, dp);
+                } else
+                        d_insert(&list[1], dp);
+        }
+        li = 1;
+
+        /* Loop until all addresses are bound or address loop detected */
+        for (i=0; list[li]!=0 && i<32; ++i, li ^= 1) {
+                /* Traverse the current list.  Bound items are put on the
+                 * `bound' list.  Unbound items are put on the next list to
+                 * traverse, `list[li^1]'.
+                 */
+                for (dp = d_rm(&list[li]); dp != 0; dp = d_rm(&list[li])){
+                        dest *newlist;
+
+                        rewrite(dp, mp);
+                        if(debug)
+                                fprint(2, "%s -> %s\n", s_to_c(dp->addr),
+                                        dp->repl1 ? s_to_c(dp->repl1):"");
+                        switch (dp->status) {
+                        case d_auth:
+                                /* authorize address if not already authorized */
+                                if(!dp->authorized){
+                                        authorize(dp);
+                                        if(dp->status==d_auth)
+                                                d_insert(&list[li^1], dp);
+                                        else
+                                                d_insert(&bound, dp);
+                                }
+                                break;
+                        case d_cat:
+                                /* address -> local */
+                                newlist = expand_local(dp);
+                                if (newlist == 0) {
+                                        /* append to mailbox (or error) */
+                                        d_same_insert(&bound, dp);
+                                } else if (newlist->status == d_undefined) {
+                                        /* Forward to ... */
+                                        d_insert(&list[li^1], newlist);
+                                } else {
+                                        /* Pipe to ... */
+                                        d_same_insert(&bound, newlist);
+                                }
+                                break;
+                        case d_pipe:
+                                /* address -> command */
+                                d_same_insert(&bound, dp);
+                                break;
+                        case d_alias:
+                                /* address -> rewritten address */
+                                newlist = s_to_dest(dp->repl1, dp);
+                                if(newlist != 0)
+                                        d_insert(&list[li^1], newlist);
+                                else
+                                        d_same_insert(&bound, dp);
+                                break;
+                        case d_translate:
+                                /* pipe to a translator */
+                                newlist = translate(dp);
+                                if (newlist != 0)
+                                        d_insert(&list[li^1], newlist);
+                                else
+                                        d_same_insert(&bound, dp);
+                                break;
+                        default:
+                                /* error */
+                                d_same_insert(&bound, dp);
+                                break;
+                        }
+                }
+        }
+
+        /* mark remaining comands as "forwarding loops" */
+        for (dp = d_rm(&list[li]); dp != 0; dp = d_rm(&list[li])) {
+                dp->status = d_loop;
+                d_same_insert(&bound, dp);
+        }
+
+        return bound;
+}
+
+/* Return TRUE if a forwarding loop exists, i.e., the String `system'
+ * is found more than 4 times in the return address.
+ */
+static int
+forward_loop(char *addr, char *system)
+{
+        int len = strlen(system), found = 0;
+
+        while (addr = strchr(addr, '!'))
+                if (!strncmp(++addr, system, len)
+                 && addr[len] == '!' && ++found == 4)
+                        return 1;
+        return 0;
+}
+
diff --git a/src/cmd/upas/send/cat_mail.c b/src/cmd/upas/send/cat_mail.c
t@@ -0,0 +1,60 @@
+#include "common.h"
+#include "send.h"
+
+
+/* dispose of local addresses */
+int
+cat_mail(dest *dp, message *mp)
+{
+        Biobuf *fp;
+        char *rcvr, *cp;
+        Mlock *l;
+        String *tmp, *s;
+        int i, n;
+
+        s = unescapespecial(s_clone(dp->repl1));
+        if (nflg) {
+                if(!xflg)
+                        print("cat >> %s\n", s_to_c(s));
+                else
+                        print("%s\n", s_to_c(dp->addr));
+                s_free(s);
+                return 0;
+        }
+        for(i = 0;; i++){
+                l = syslock(s_to_c(s));
+                if(l == 0)
+                        return refuse(dp, mp, "can't lock mail file", 0, 0);
+
+                fp = sysopen(s_to_c(s), "al", MBOXMODE);
+                if(fp)
+                        break;
+                tmp = s_append(0, s_to_c(s));
+                s_append(tmp, ".tmp");
+                fp = sysopen(s_to_c(tmp), "al", MBOXMODE);
+                if(fp){
+                        syslog(0, "mail", "error: used %s", s_to_c(tmp));
+                        s_free(tmp);
+                        break;
+                }
+                s_free(tmp);
+                sysunlock(l);
+                if(i >= 5)
+                        return refuse(dp, mp, "mail file cannot be opened", 0, 0);
+                sleep(1000);
+        }
+        s_free(s);
+        n = m_print(mp, fp, (char *)0, 1);
+        if (Bprint(fp, "\n") < 0 || Bflush(fp) < 0 || n < 0){
+                sysclose(fp);
+                sysunlock(l);
+                return refuse(dp, mp, "error writing mail file", 0, 0);
+        }
+        sysclose(fp);
+        sysunlock(l);
+        rcvr = s_to_c(dp->addr);
+        if(cp = strrchr(rcvr, '!'))
+                rcvr = cp+1;
+        logdelivery(dp, rcvr, mp);
+        return 0;
+}
diff --git a/src/cmd/upas/send/dest.c b/src/cmd/upas/send/dest.c
t@@ -0,0 +1,260 @@
+#include "common.h"
+#include "send.h"
+
+static String* s_parseq(String*, String*);
+
+/* exports */
+dest *dlist;
+
+extern dest*
+d_new(String *addr)
+{
+        dest *dp;
+
+        dp = (dest *)mallocz(sizeof(dest), 1);
+        if (dp == 0) {
+                perror("d_new");
+                exit(1);
+        }
+        dp->same = dp;
+        dp->nsame = 1;
+        dp->nchar = 0;
+        dp->next = dp;
+        dp->addr = escapespecial(addr);
+        dp->parent = 0;
+        dp->repl1 = dp->repl2 = 0;
+        dp->status = d_undefined;
+        return dp;
+}
+
+extern void
+d_free(dest *dp)
+{
+        if (dp != 0) {
+                s_free(dp->addr);
+                s_free(dp->repl1);
+                s_free(dp->repl2);
+                free((char *)dp);
+        }
+}
+
+/* The following routines manipulate an ordered list of items.  Insertions
+ * are always to the end of the list.  Deletions are from the beginning.
+ *
+ * The list are circular witht the `head' of the list being the last item
+ * added.
+ */
+
+/*  Get first element from a circular list linked via 'next'. */
+extern dest *
+d_rm(dest **listp)
+{
+        dest *dp;
+
+        if (*listp == 0)
+                return 0;
+        dp = (*listp)->next;
+        if (dp == *listp)
+                *listp = 0;
+        else
+                (*listp)->next = dp->next;
+        dp->next = dp;
+        return dp;
+}
+
+/*  Insert a new entry at the end of the list linked via 'next'. */
+extern void
+d_insert(dest **listp, dest *new)
+{
+        dest *head;
+
+        if (*listp == 0) {
+                *listp = new;
+                return;
+        }
+        if (new == 0)
+                return;
+        head = new->next;
+        new->next = (*listp)->next;
+        (*listp)->next = head;
+        *listp = new;
+        return;
+}
+
+/*  Get first element from a circular list linked via 'same'. */
+extern dest *
+d_rm_same(dest **listp)
+{
+        dest *dp;
+
+        if (*listp == 0)
+                return 0;
+        dp = (*listp)->same;
+        if (dp == *listp)
+                *listp = 0;
+        else
+                (*listp)->same = dp->same;
+        dp->same = dp;
+        return dp;
+}
+
+/* Look for a duplicate on the same list */
+int
+d_same_dup(dest *dp, dest *new)
+{
+        dest *first = dp;
+
+        if(new->repl2 == 0)
+                return 1;
+        do {
+                if(strcmp(s_to_c(dp->repl2), s_to_c(new->repl2))==0)
+                        return 1;
+                dp = dp->same;
+        } while(dp != first);
+        return 0;
+}
+
+/* Insert an entry into the corresponding list linked by 'same'.  Note that
+ * the basic structure is a list of lists.
+ */
+extern void
+d_same_insert(dest **listp, dest *new)
+{
+        dest *dp;
+        int len;
+
+        if(new->status == d_pipe || new->status == d_cat) {
+                len = new->repl2 ? strlen(s_to_c(new->repl2)) : 0;
+                if(*listp != 0){
+                        dp = (*listp)->next;
+                        do {
+                                if(dp->status == new->status
+                                && strcmp(s_to_c(dp->repl1), s_to_c(new->repl1))==0){
+                                        /* remove duplicates */
+                                        if(d_same_dup(dp, new))
+                                                return;
+                                        /* add to chain if chain small enough */
+                                        if(dp->nsame < MAXSAME
+                                        && dp->nchar + len < MAXSAMECHAR){
+                                                new->same = dp->same;
+                                                dp->same = new;
+                                                dp->nchar += len + 1;
+                                                dp->nsame++;
+                                                return;
+                                        }
+                                }
+                                dp = dp->next;
+                        } while (dp != (*listp)->next);
+                }
+                new->nchar = strlen(s_to_c(new->repl1)) + len + 1;
+        }
+        new->next = new;
+        d_insert(listp, new);
+}
+
+/*
+ *  Form a To: if multiple destinations.
+ *  The local! and !local! checks are artificial intelligence,
+ *  there should be a better way.
+ */
+extern String*
+d_to(dest *list)
+{
+        dest *np, *sp;
+        String *s;
+        int i, n;
+        char *cp;
+
+        s = s_new();
+        s_append(s, "To: ");
+        np = list;
+        i = n = 0;
+        do {
+                np = np->next;
+                sp = np;
+                do {
+                        sp = sp->same;
+                        cp = s_to_c(sp->addr);
+
+                        /* hack to get local! out of the names */
+                        if(strncmp(cp, "local!", 6) == 0)
+                                cp += 6;
+
+                        if(n > 20){        /* 20 to appease mailers complaining about long lines */
+                                s_append(s, "\n\t");
+                                n = 0;
+                        }
+                        if(i != 0){
+                                s_append(s, ", ");
+                                n += 2;
+                        }
+                        s_append(s, cp);
+                        n += strlen(cp);
+                        i++;
+                } while(sp != np);
+        } while(np != list);
+
+        return unescapespecial(s);
+}
+
+/* expand a String of destinations into a linked list of destiniations */
+extern dest *
+s_to_dest(String *sp, dest *parent)
+{
+        String *addr;
+        dest *list=0;
+        dest *new;
+
+        if (sp == 0)
+                return 0;
+        addr = s_new();
+        while (s_parseq(sp, addr)!=0) {
+                addr = escapespecial(addr);
+                if(shellchars(s_to_c(addr))){
+                        while(new = d_rm(&list))
+                                d_free(new);
+                        break;
+                }
+                new = d_new(addr);
+                new->parent = parent;
+                new->authorized = parent->authorized;
+                d_insert(&list, new);
+                addr = s_new();
+        }
+        s_free(addr);
+        return list;
+}
+
+#undef isspace
+#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
+
+/*  Get the next field from a String.  The field is delimited by white space.
+ *  Anything delimited by double quotes is included in the string.
+ */
+static String*
+s_parseq(String *from, String *to)
+{
+        int c;
+
+        if (*from->ptr == '\0')
+                return 0;
+        if (to == 0)
+                to = s_new();
+        for (c = *from->ptr;!isspace(c) && c != 0; c = *(++from->ptr)){
+                s_putc(to, c);
+                if(c == '"'){
+                        for (c = *(++from->ptr); c && c != '"'; c = *(++from->ptr))
+                                s_putc(to, *from->ptr);
+                        s_putc(to, '"');
+                        if(c == 0)
+                                break;
+                }
+        }
+        s_terminate(to);
+
+        /* crunch trailing white */
+        while(isspace(*from->ptr))
+                from->ptr++;
+
+        return to;
+}
diff --git a/src/cmd/upas/send/filter.c b/src/cmd/upas/send/filter.c
t@@ -0,0 +1,128 @@
+#include "common.h"
+#include "send.h"
+
+Biobuf        bin;
+int rmail, tflg;
+char *subjectarg;
+
+char *findbody(char*);
+
+void
+main(int argc, char *argv[])
+{
+        message *mp;
+        dest *dp;
+        Reprog *p;
+        Resub match[10];
+        char file[MAXPATHLEN];
+        Biobuf *fp;
+        char *rcvr, *cp;
+        Mlock *l;
+        String *tmp;
+        int i;
+        int header, body;
+
+        header = body = 0;
+        ARGBEGIN {
+        case 'h':
+                header = 1;
+                break;
+        case 'b':
+                header = 1;
+                body = 1;
+                break;
+        } ARGEND
+
+        Binit(&bin, 0, OREAD);
+        if(argc < 2){
+                fprint(2, "usage: filter rcvr mailfile [regexp mailfile ...]\n");
+                exits("usage");
+        }
+        mp = m_read(&bin, 1, 0);
+
+        /* get rid of local system name */
+        cp = strchr(s_to_c(mp->sender), '!');
+        if(cp){
+                cp++;
+                mp->sender = s_copy(cp);
+        }
+
+        dp = d_new(s_copy(argv[0]));
+        strecpy(file, file+sizeof file, argv[1]);
+        cp = findbody(s_to_c(mp->body));
+        for(i = 2; i < argc; i += 2){
+                p = regcomp(argv[i]);
+                if(p == 0)
+                        continue;
+                if(regexec(p, s_to_c(mp->sender), match, 10)){
+                        regsub(argv[i+1], file, sizeof(file), match, 10);
+                        break;
+                }
+                if(header == 0 && body == 0)
+                        continue;
+                if(regexec(p, s_to_c(mp->body), match, 10)){
+                        if(body == 0 && match[0].s.sp >= cp)
+                                continue;
+                        regsub(argv[i+1], file, sizeof(file), match, 10);
+                        break;
+                }
+        }
+
+        /*
+         *  always lock the normal mail file to avoid too many lock files
+         *  lying about.  This isn't right but it's what the majority prefers.
+         */
+        l = syslock(argv[1]);
+        if(l == 0){
+                fprint(2, "can't lock mail file %s\n", argv[1]);
+                exit(1);
+        }
+
+        /*
+         *  open the destination mail file
+         */
+        fp = sysopen(file, "ca", MBOXMODE);
+        if (fp == 0){
+                tmp = s_append(0, file);
+                s_append(tmp, ".tmp");
+                fp = sysopen(s_to_c(tmp), "cal", MBOXMODE);
+                if(fp == 0){
+                        sysunlock(l);
+                        fprint(2, "can't open mail file %s\n", file);
+                        exit(1);
+                }
+                syslog(0, "mail", "error: used %s", s_to_c(tmp));
+                s_free(tmp);
+        }
+        Bseek(fp, 0, 2);
+        if(m_print(mp, fp, (char *)0, 1) < 0
+        || Bprint(fp, "\n") < 0
+        || Bflush(fp) < 0){
+                sysclose(fp);
+                sysunlock(l);
+                fprint(2, "can't write mail file %s\n", file);
+                exit(1);
+        }
+        sysclose(fp);
+
+        sysunlock(l);
+        rcvr = argv[0];
+        if(cp = strrchr(rcvr, '!'))
+                rcvr = cp+1;
+        logdelivery(dp, rcvr, mp);
+        exit(0);
+}
+
+char*
+findbody(char *p)
+{
+        if(*p == '\n')
+                return p;
+
+        while(*p){
+                if(*p == '\n' && *(p+1) == '\n')
+                        return p+1;
+                p++;
+        }
+        return p;
+}
diff --git a/src/cmd/upas/send/gateway.c b/src/cmd/upas/send/gateway.c
t@@ -0,0 +1,24 @@
+#include "common.h"
+#include "send.h"
+
+#undef isspace
+#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
+
+/*
+ *  Translate the last component of the sender address.  If the translation
+ *  yields the same address, replace the sender with its last component.
+ */
+extern void
+gateway(message *mp)
+{
+        char *base;
+        String *s;
+
+        /* first remove all systems equivalent to us */
+        base = skipequiv(s_to_c(mp->sender));
+        if(base != s_to_c(mp->sender)){
+                s = mp->sender;
+                mp->sender = s_copy(base);
+                s_free(s);
+        }
+}
diff --git a/src/cmd/upas/send/local.c b/src/cmd/upas/send/local.c
t@@ -0,0 +1,129 @@
+#include "common.h"
+#include "send.h"
+
+static void
+mboxfile(dest *dp, String *user, String *path, char *file)
+{
+        char *cp;
+
+        mboxpath(s_to_c(user), s_to_c(dp->addr), path, 0);
+        cp = strrchr(s_to_c(path), '/');
+        if(cp)
+                path->ptr = cp+1;
+        else
+                path->ptr = path->base;
+        s_append(path, file);
+}
+
+/*
+ *  Check forwarding requests
+ */
+extern dest*
+expand_local(dest *dp)
+{
+        Biobuf *fp;
+        String *file, *line, *s;
+        dest *rv;
+        int forwardok;
+        char *user;
+
+        /* short circuit obvious security problems */
+        if(strstr(s_to_c(dp->addr), "/../")){
+                dp->status = d_unknown;
+                return 0;
+        }
+
+        /* isolate user's name if part of a path */
+        user = strrchr(s_to_c(dp->addr), '!');
+        if(user)
+                user++;
+        else
+                user = s_to_c(dp->addr);
+
+        /* if no replacement string, plug in user's name */
+        if(dp->repl1 == 0){
+                dp->repl1 = s_new();
+                mboxname(user, dp->repl1);
+        }
+
+        s = unescapespecial(s_clone(dp->repl1));
+
+        /*
+         *  if this is the descendant of a `forward' file, don't
+         *  look for a forward.
+         */
+        forwardok = 1;
+        for(rv = dp->parent; rv; rv = rv->parent)
+                if(rv->status == d_cat){
+                        forwardok = 0;
+                        break;
+                }
+        file = s_new();
+        if(forwardok){
+                /*
+                 *  look for `forward' file for forwarding address(es)
+                 */
+                mboxfile(dp, s, file, "forward");
+                fp = sysopen(s_to_c(file), "r", 0);
+                if (fp != 0) {
+                        line = s_new();
+                        for(;;){
+                                if(s_read_line(fp, line) == nil)
+                                        break;
+                                if(*(line->ptr - 1) != '\n')
+                                        break;
+                                if(*(line->ptr - 2) == '\\')
+                                        *(line->ptr-2) = ' ';
+                                *(line->ptr-1) = ' ';
+                        }
+                        sysclose(fp);
+                        if(debug)
+                                fprint(2, "forward = %s\n", s_to_c(line));
+                        rv = s_to_dest(s_restart(line), dp);
+                        s_free(line);
+                        if(rv){
+                                s_free(file);
+                                s_free(s);
+                                return rv;
+                        }
+                }
+        }
+
+        /*
+         *  look for a 'pipe' file.  This won't work if there are
+         *  special characters in the account name since the file
+         *  name passes through a shell.  tdb.
+         */
+        mboxfile(dp, dp->repl1, s_reset(file), "pipeto");
+        if(sysexist(s_to_c(file))){
+                if(debug)
+                        fprint(2, "found a pipeto file\n");
+                dp->status = d_pipeto;
+                line = s_new();
+                s_append(line, "upasname='");
+                s_append(line, user);
+                s_append(line, "' ");
+                s_append(line, s_to_c(file));
+                s_append(line, " ");
+                s_append(line, s_to_c(dp->addr));
+                s_append(line, " ");
+                s_append(line, s_to_c(dp->repl1));
+                s_free(dp->repl1);
+                dp->repl1 = line;
+                s_free(file);
+                s_free(s);
+                return dp;
+        }
+
+        /*
+         *  see if the mailbox directory exists
+         */
+        mboxfile(dp, s, s_reset(file), ".");
+        if(sysexist(s_to_c(file)))
+                dp->status = d_cat;
+        else
+                dp->status = d_unknown;
+        s_free(file);
+        s_free(s);
+        return 0;
+}
diff --git a/src/cmd/upas/send/log.c b/src/cmd/upas/send/log.c
t@@ -0,0 +1,85 @@
+#include "common.h"
+#include "send.h"
+
+/* configuration */
+#define LOGBiobuf "log/status"
+
+/* log mail delivery */
+extern void
+logdelivery(dest *list, char *rcvr, message *mp)
+{
+        dest *parent;
+        String *srcvr, *sender;
+
+        srcvr = unescapespecial(s_copy(rcvr));
+        sender = unescapespecial(s_clone(mp->sender));
+
+        for(parent=list; parent->parent!=0; parent=parent->parent)
+                ;
+        if(parent!=list && strcmp(s_to_c(parent->addr), s_to_c(srcvr))!=0)
+                syslog(0, "mail", "delivered %s From %.256s %.256s (%.256s) %d",
+                        rcvr,
+                        s_to_c(sender), s_to_c(mp->date),
+                        s_to_c(parent->addr), mp->size);
+        else
+                syslog(0, "mail", "delivered %s From %.256s %.256s %d", s_to_c(srcvr),
+                        s_to_c(sender), s_to_c(mp->date), mp->size);
+        s_free(srcvr);
+        s_free(sender);
+}
+
+/* log mail forwarding */
+extern void
+loglist(dest *list, message *mp, char *tag)
+{
+        dest *next;
+        dest *parent;
+        String *srcvr, *sender;
+
+        sender = unescapespecial(s_clone(mp->sender));
+
+        for(next=d_rm(&list); next != 0; next = d_rm(&list)) {
+                for(parent=next; parent->parent!=0; parent=parent->parent)
+                        ;
+                srcvr = unescapespecial(s_clone(next->addr));
+                if(parent!=next)
+                        syslog(0, "mail", "%s %.256s From %.256s %.256s (%.256s) %d",
+                                tag,
+                                s_to_c(srcvr), s_to_c(sender),
+                                s_to_c(mp->date), s_to_c(parent->addr), mp->size);
+                else
+                        syslog(0, "mail", "%s %.256s From %.256s %.256s %d", tag,
+                                s_to_c(srcvr), s_to_c(sender),
+                                s_to_c(mp->date), mp->size);
+                s_free(srcvr);
+        }
+        s_free(sender);
+}
+
+/* log a mail refusal */
+extern void
+logrefusal(dest *dp, message *mp, char *msg)
+{
+        char buf[2048];
+        char *cp, *ep;
+        String *sender, *srcvr;
+
+        srcvr = unescapespecial(s_clone(dp->addr));
+        sender = unescapespecial(s_clone(mp->sender));
+
+        sprint(buf, "error %.256s From %.256s %.256s\nerror+ ", s_to_c(srcvr),
+                s_to_c(sender), s_to_c(mp->date));
+        s_free(srcvr);
+        s_free(sender);
+        cp = buf + strlen(buf);
+        ep = buf + sizeof(buf) - sizeof("error + ");
+        while(*msg && cp
diff --git a/src/cmd/upas/send/main.c b/src/cmd/upas/send/main.c
t@@ -0,0 +1,575 @@
+#include "common.h"
+#include "send.h"
+
+/* globals to all files */
+int rmail;
+char *thissys, *altthissys;
+int nflg;
+int xflg;
+int debug;
+int rflg;
+int iflg = 1;
+int nosummary;
+
+/* global to this file */
+static String *errstring;
+static message *mp;
+static int interrupt;
+static int savemail;
+static Biobuf in;
+static int forked;
+static int add822headers = 1;
+static String *arglist;
+
+/* predeclared */
+static int        send(dest *, message *, int);
+static void        lesstedious(void);
+static void        save_mail(message *);
+static int        complain_mail(dest *, message *);
+static int        pipe_mail(dest *, message *);
+static void        appaddr(String *, dest *);
+static void        mkerrstring(String *, message *, dest *, dest *, char *, int);
+static int        replymsg(String *, message *, dest *);
+static int        catchint(void*, char*);
+
+void
+usage(void)
+{
+        fprint(2, "usage: mail [-birtx] list-of-addresses\n");
+        exits("usage");
+}
+
+void
+main(int argc, char *argv[])
+{
+        dest *dp=0;
+        int checkforward;
+        char *base;
+        int rv;
+
+        /* process args */
+        ARGBEGIN{
+        case '#':
+                nflg = 1;
+                break;
+        case 'b':
+                add822headers = 0;
+                break;
+        case 'x':
+                nflg = 1;
+                xflg = 1;
+                break;
+        case 'd':
+                debug = 1;
+                break;
+        case 'i':
+                iflg = 0;
+                break;
+        case 'r':
+                rflg = 1;
+                break;
+        default:
+                usage();
+        }ARGEND
+
+        while(*argv){
+                if(shellchars(*argv)){
+                        fprint(2, "illegal characters in destination\n");
+                        exits("syntax");
+                }
+                d_insert(&dp, d_new(s_copy(*argv++)));
+        }
+
+        if (dp == 0)
+                usage();
+        arglist = d_to(dp);
+
+        /*
+         * get context:
+         *        - whether we're rmail or mail
+         */
+        base = basename(argv0);
+        checkforward = rmail = (strcmp(base, "rmail")==0) | rflg;
+        thissys = sysname_read();
+        altthissys = alt_sysname_read();
+        if(rmail)
+                add822headers = 0;
+
+        /*
+         *  read the mail.  If an interrupt occurs while reading, save in
+         *  dead.letter
+         */
+        if (!nflg) {
+                Binit(&in, 0, OREAD);
+                if(!rmail)
+                        atnotify(catchint, 1);
+                mp = m_read(&in, rmail, !iflg);
+                if (mp == 0)
+                        exit(0);
+                if (interrupt != 0) {
+                        save_mail(mp);
+                        exit(1);
+                }
+        } else {
+                mp = m_new();
+                if(default_from(mp) < 0){
+                        fprint(2, "%s: can't determine login name\n", argv0);
+                        exit(1);
+                }
+        }
+        errstring = s_new();
+        getrules();
+
+        /*
+         *  If this is a gateway, translate the sender address into a local
+         *  address.  This only happens if mail to the local address is 
+         *  forwarded to the sender.
+         */
+        gateway(mp);
+
+        /*
+         *  Protect against shell characters in the sender name for
+         *  security reasons.
+         */
+        mp->sender = escapespecial(mp->sender);
+        if (shellchars(s_to_c(mp->sender)))
+                mp->replyaddr = s_copy("postmaster");
+        else
+                mp->replyaddr = s_clone(mp->sender);
+
+        /*
+         *  reject messages that have been looping for too long
+         */
+        if(mp->received > 32)
+                exit(refuse(dp, mp, "possible forward loop", 0, 0));
+
+        /*
+         *  reject messages that are too long.  We don't do it earlier
+         *  in m_read since we haven't set up enough things yet.
+         */
+        if(mp->size < 0)
+                exit(refuse(dp, mp, "message too long", 0, 0));
+
+        rv = send(dp, mp, checkforward);
+        if(savemail)
+                save_mail(mp);
+        if(mp)
+                m_free(mp);
+        exit(rv);
+}
+
+/* send a message to a list of sites */
+static int
+send(dest *destp, message *mp, int checkforward)
+{
+        dest *dp;                /* destination being acted upon */
+        dest *bound;                /* bound destinations */
+        int errors=0;
+
+        /* bind the destinations to actions */
+        bound = up_bind(destp, mp, checkforward);
+        if(add822headers && mp->haveto == 0){
+                if(nosummary)
+                        mp->to = d_to(bound);
+                else
+                        mp->to = arglist;
+        }
+
+        /* loop through and execute commands */
+        for (dp = d_rm(&bound); dp != 0; dp = d_rm(&bound)) {
+                switch (dp->status) {
+                case d_cat:
+                        errors += cat_mail(dp, mp);
+                        break;
+                case d_pipeto:
+                case d_pipe:
+                        if (!rmail && !nflg && !forked) {
+                                forked = 1;
+                                lesstedious();
+                        }
+                        errors += pipe_mail(dp, mp);
+                        break;
+                default:
+                        errors += complain_mail(dp, mp);
+                        break;
+                }
+        }
+
+        return errors;
+}
+
+/* avoid user tedium (as Mike Lesk said in a previous version) */
+static void
+lesstedious(void)
+{
+        int i;
+
+        if(debug)
+                return;
+
+        switch(fork()){
+        case -1:
+                break;
+        case 0:
+                sysdetach();
+                for(i=0; i<3; i++)
+                        close(i);
+                savemail = 0;
+                break;
+        default:
+                exit(0);
+        }
+}
+
+
+/* save the mail */
+static void
+save_mail(message *mp)
+{
+        Biobuf *fp;
+        String *file;
+
+        file = s_new();
+        deadletter(file);
+        fp = sysopen(s_to_c(file), "cAt", 0660);
+        if (fp == 0)
+                return;
+        m_bprint(mp, fp);
+        sysclose(fp);
+        fprint(2, "saved in %s\n", s_to_c(file));
+        s_free(file);
+}
+
+/* remember the interrupt happened */
+
+static int
+catchint(void *a, char *msg)
+{
+        USED(a);
+        if(strstr(msg, "interrupt") || strstr(msg, "hangup")) {
+                interrupt = 1;
+                return 1;
+        }
+        return 0;
+}
+
+/* dispose of incorrect addresses */
+static int
+complain_mail(dest *dp, message *mp)
+{
+        char *msg;
+
+        switch (dp->status) {
+        case d_undefined:
+                msg = "Invalid address"; /* a little different, for debugging */
+                break;
+        case d_syntax:
+                msg = "invalid address";
+                break;
+        case d_unknown:
+                msg = "unknown user";
+                break;
+        case d_eloop:
+        case d_loop:
+                msg = "forwarding loop";
+                break;
+        case d_noforward:
+                if(dp->pstat && *s_to_c(dp->repl2))
+                        return refuse(dp, mp, s_to_c(dp->repl2), dp->pstat, 0);
+                else
+                        msg = "destination unknown or forwarding disallowed";
+                break;
+        case d_pipe:
+                msg = "broken pipe";
+                break;
+        case d_cat:
+                msg = "broken cat";
+                break;
+        case d_translate:
+                if(dp->pstat && *s_to_c(dp->repl2))
+                        return refuse(dp, mp, s_to_c(dp->repl2), dp->pstat, 0);
+                else
+                        msg = "name translation failed";
+                break;
+        case d_alias:
+                msg = "broken alias";
+                break;
+        case d_badmbox:
+                msg = "corrupted mailbox";
+                break;
+        case d_resource:
+                return refuse(dp, mp, "out of some resource.  Try again later.", 0, 1);
+        default:
+                msg = "unknown d_";
+                break;
+        }
+        if (nflg) {
+                print("%s: %s\n", msg, s_to_c(dp->addr));
+                return 0;
+        }
+        return refuse(dp, mp, msg, 0, 0);
+}
+
+/* dispose of remote addresses */
+static int
+pipe_mail(dest *dp, message *mp)
+{
+        dest *next, *list=0;
+        String *cmd;
+        process *pp;
+        int status;
+        char *none;
+        String *errstring=s_new();
+
+        if (dp->status == d_pipeto)
+                none = "none";
+        else
+                none = 0;
+        /*
+         *  collect the arguments
+         */
+        next = d_rm_same(&dp);
+        if(xflg)
+                cmd = s_new();
+        else
+                cmd = s_clone(next->repl1);
+        for(; next != 0; next = d_rm_same(&dp)){
+                if(xflg){
+                        s_append(cmd, s_to_c(next->addr));
+                        s_append(cmd, "\n");
+                } else {
+                        if (next->repl2 != 0) {
+                                s_append(cmd, " ");
+                                s_append(cmd, s_to_c(next->repl2));
+                        }
+                }
+                d_insert(&list, next);
+        }
+
+        if (nflg) {
+                if(xflg)
+                        print("%s", s_to_c(cmd));
+                else
+                        print("%s\n", s_to_c(cmd));
+                s_free(cmd);
+                return 0;
+        }
+
+        /*
+         *  run the process
+         */
+        pp = proc_start(s_to_c(cmd), instream(), 0, outstream(), 1, none);
+        if(pp==0 || pp->std[0]==0 || pp->std[2]==0)
+                return refuse(list, mp, "out of processes, pipes, or memory", 0, 1);
+        pipesig(0);
+        m_print(mp, pp->std[0]->fp, thissys, 0);
+        pipesigoff();
+        stream_free(pp->std[0]);
+        pp->std[0] = 0;
+        while(s_read_line(pp->std[2]->fp, errstring))
+                ;
+        status = proc_wait(pp);
+        proc_free(pp);
+        s_free(cmd);
+
+        /*
+         *  return status
+         */
+        if (status != 0)
+                return refuse(list, mp, s_to_c(errstring), status, 0);
+        loglist(list, mp, "remote");
+        return 0;
+}
+
+static void
+appaddr(String *sp, dest *dp)
+{
+        dest *parent;
+        String *s;
+
+        if (dp->parent != 0) {
+                for(parent=dp->parent; parent->parent!=0; parent=parent->parent)
+                        ;
+                s = unescapespecial(s_clone(parent->addr));
+                s_append(sp, s_to_c(s));
+                s_free(s);
+                s_append(sp, "' alias `");
+        }
+        s = unescapespecial(s_clone(dp->addr));
+        s_append(sp, s_to_c(s));
+        s_free(s);
+}
+
+/*
+ *  reject delivery
+ *
+ *  returns        0        - if mail has been disposed of
+ *                other        - if mail has not been disposed
+ */
+int
+refuse(dest *list, message *mp, char *cp, int status, int outofresources)
+{
+        String *errstring=s_new();
+        dest *dp;
+        int rv;
+
+        dp = d_rm(&list);
+        mkerrstring(errstring, mp, dp, list, cp, status);
+
+        /*
+         *  log first in case we get into trouble
+         */
+        logrefusal(dp, mp, s_to_c(errstring));
+
+        /*
+         *  bulk mail is never replied to, if we're out of resources,
+         *  let the sender try again
+         */
+        if(rmail){
+                /* accept it or request a retry */
+                if(outofresources){
+                        fprint(2, "Mail %s\n", s_to_c(errstring));
+                        rv = 1;                                        /* try again later */
+                } else if(mp->bulk)
+                        rv = 0;                                        /* silently discard bulk */
+                else
+                        rv = replymsg(errstring, mp, dp);        /* try later if we can't reply */
+        } else {
+                /* aysnchronous delivery only happens if !rmail */
+                if(forked){
+                        /*
+                         *  if spun off for asynchronous delivery, we own the mail now.
+                         *  return it or dump it on the floor.  rv really doesn't matter.
+                         */
+                        rv = 0;
+                        if(!outofresources && !mp->bulk)
+                                replymsg(errstring, mp, dp);
+                } else {
+                        fprint(2, "Mail %s\n", s_to_c(errstring));
+                        savemail = 1;
+                        rv = 1;
+                }
+        }
+
+        s_free(errstring);
+        return rv;
+}
+
+/* make the error message */
+static void
+mkerrstring(String *errstring, message *mp, dest *dp, dest *list, char *cp, int status)
+{
+        dest *next;
+        char smsg[64];
+        String *sender;
+
+        sender = unescapespecial(s_clone(mp->sender));
+
+        /* list all aliases */
+        s_append(errstring, " from '");
+        s_append(errstring, s_to_c(sender));
+        s_append(errstring, "'\nto '");
+        appaddr(errstring, dp);
+        for(next = d_rm(&list); next != 0; next = d_rm(&list)) {
+                s_append(errstring, "'\nand '");
+                appaddr(errstring, next);
+                d_insert(&dp, next);
+        }
+        s_append(errstring, "'\nfailed with error '");
+        s_append(errstring, cp);
+        s_append(errstring, "'.\n");
+
+        /* >> and | deserve different flavored messages */
+        switch(dp->status) {
+        case d_pipe:
+                s_append(errstring, "The mailer `");
+                s_append(errstring, s_to_c(dp->repl1));
+                sprint(smsg, "' returned error status %x.\n\n", status);
+                s_append(errstring, smsg);
+                break;
+        }
+
+        s_free(sender);
+}
+
+/*
+ *  create a new boundary
+ */
+static String*
+mkboundary(void)
+{
+        char buf[32];
+        int i;
+        static int already;
+
+        if(already == 0){
+                srand((time(0)<<16)|getpid());
+                already = 1;
+        }
+        strcpy(buf, "upas-");
+        for(i = 5; i < sizeof(buf)-1; i++)
+                buf[i] = 'a' + nrand(26);
+        buf[i] = 0;
+        return s_copy(buf);
+}
+
+/*
+ *  reply with up to 1024 characters of the
+ *  original message
+ */
+static int
+replymsg(String *errstring, message *mp, dest *dp)
+{
+        message *refp = m_new();
+        dest *ndp;
+        char *rcvr;
+        int rv;
+        String *boundary;
+
+        boundary = mkboundary();
+
+        refp->bulk = 1;
+        refp->rfc822headers = 1;
+        rcvr = dp->status==d_eloop ? "postmaster" : s_to_c(mp->replyaddr);
+        ndp = d_new(s_copy(rcvr));
+        s_append(refp->sender, "postmaster");
+        s_append(refp->replyaddr, "/dev/null");
+        s_append(refp->date, thedate());
+        refp->haveto = 1;
+        s_append(refp->body, "To: ");
+        s_append(refp->body, rcvr);
+        s_append(refp->body, "\n");
+        s_append(refp->body, "Subject: bounced mail\n");
+        s_append(refp->body, "MIME-Version: 1.0\n");
+        s_append(refp->body, "Content-Type: multipart/mixed;\n");
+        s_append(refp->body, "\tboundary=\"");
+        s_append(refp->body, s_to_c(boundary));
+        s_append(refp->body, "\"\n");
+        s_append(refp->body, "Content-Disposition: inline\n");
+        s_append(refp->body, "\n");
+        s_append(refp->body, "This is a multi-part message in MIME format.\n");
+        s_append(refp->body, "--");
+        s_append(refp->body, s_to_c(boundary));
+        s_append(refp->body, "\n");
+        s_append(refp->body, "Content-Disposition: inline\n");
+        s_append(refp->body, "Content-Type: text/plain; charset=\"US-ASCII\"\n");
+        s_append(refp->body, "Content-Transfer-Encoding: 7bit\n");
+        s_append(refp->body, "\n");
+        s_append(refp->body, "The attached mail");
+        s_append(refp->body, s_to_c(errstring));
+        s_append(refp->body, "--");
+        s_append(refp->body, s_to_c(boundary));
+        s_append(refp->body, "\n");
+        s_append(refp->body, "Content-Type: message/rfc822\n");
+        s_append(refp->body, "Content-Disposition: inline\n\n");
+        s_append(refp->body, s_to_c(mp->body));
+        s_append(refp->body, "--");
+        s_append(refp->body, s_to_c(boundary));
+        s_append(refp->body, "--\n");
+
+        refp->size = s_len(refp->body);
+        rv = send(ndp, refp, 0);
+        m_free(refp);
+        d_free(ndp);
+        return rv;
+}
diff --git a/src/cmd/upas/send/makefile b/src/cmd/upas/send/makefile
t@@ -0,0 +1,46 @@
+SSRC=        message.c main.c bind.c rewrite.c local.c dest.c process.c translate.c\
+        log.c chkfwd.c notify.c gateway.c authorize.o ../common/*.c
+SOBJ=        message.o main.o bind.o rewrite.o local.o dest.o process.o translate.o\
+        log.o chkfwd.o notify.o gateway.o authorize.o\
+        ../config/config.o ../common/common.a ../libc/libc.a
+SINC=        ../common/mail.h ../common/string.h ../common/aux.h
+CFLAGS=${UNIX} -g -I. -I../libc -I../common -I/usr/include ${SCFLAGS}
+LFLAGS=-g
+.c.o: ; $(CC) -c $(CFLAGS) $*.c
+LIB=/usr/lib/upas
+
+all: send
+
+send: $(SOBJ)
+        $(CC) $(SOBJ) $(LFLAGS) -o send
+
+chkfwd.o: $(SINC) message.h dest.h
+dest.o: $(SINC) dest.h
+local.o: $(SINC) dest.h process.h
+log.o: $(SINC) message.h
+main.o: $(SINC) message.h dest.h process.h
+bind.o: $(SINC) dest.h message.h
+process.o: $(SINC) process.h
+rewrite.o: $(SINC) dest.h
+translate.o: $(SINC) dest.h process.h
+message.o: $(SINC) message.h
+notify.o: $(SINC) message.h
+gateway.o: $(SINC) dest.h message.h
+
+prcan:
+        prcan $(SSRC)
+
+clean:
+        -rm -f send *.[oO] a.out core *.sL rmail
+
+cyntax:
+        cyntax $(CFLAGS) $(SSRC)
+
+install: send
+        rm -f $(LIB)/send /bin/rmail
+        cp send $(LIB)/send
+        cp send /bin/rmail
+        strip /bin/rmail
+        strip $(LIB)/send
+        chown root $(LIB)/send /bin/rmail
+        chmod 4755 $(LIB)/send /bin/rmail
diff --git a/src/cmd/upas/send/message.c b/src/cmd/upas/send/message.c
t@@ -0,0 +1,573 @@
+#include "common.h"
+#include "send.h"
+
+#include "../smtp/smtp.h"
+#include "../smtp/y.tab.h"
+
+/* global to this file */
+static Reprog *rfprog;
+static Reprog *fprog;
+
+#define VMLIMIT (64*1024)
+#define MSGLIMIT (128*1024*1024)
+
+int received;        /* from rfc822.y */
+
+static String*        getstring(Node *p);
+static String*        getaddr(Node *p);
+
+extern int
+default_from(message *mp)
+{
+        char *cp, *lp;
+
+        cp = getenv("upasname");
+        lp = getlog();
+        if(lp == nil)
+                return -1;
+
+        if(cp && *cp)
+                s_append(mp->sender, cp);
+        else
+                s_append(mp->sender, lp);
+        s_append(mp->date, thedate());
+        return 0;
+}
+
+extern message *
+m_new(void)
+{
+        message *mp;
+
+        mp = (message *)mallocz(sizeof(message), 1);
+        if (mp == 0) {
+                perror("message:");
+                exit(1);
+        }
+        mp->sender = s_new();
+        mp->replyaddr = s_new();
+        mp->date = s_new();
+        mp->body = s_new();
+        mp->size = 0;
+        mp->fd = -1;
+        return mp;
+}
+
+extern void
+m_free(message *mp)
+{
+        if(mp->fd >= 0){
+                close(mp->fd);
+                sysremove(s_to_c(mp->tmp));
+                s_free(mp->tmp);
+        }
+        s_free(mp->sender);
+        s_free(mp->date);
+        s_free(mp->body);
+        s_free(mp->havefrom);
+        s_free(mp->havesender);
+        s_free(mp->havereplyto);
+        s_free(mp->havesubject);
+        free((char *)mp);
+}
+
+/* read a message into a temp file , return an open fd to it */
+static int
+m_read_to_file(Biobuf *fp, message *mp)
+{
+        int fd;
+        int n;
+        String *file;
+        char buf[4*1024];
+
+        file = s_new();
+        /*
+         *  create temp file to be remove on close
+         */
+        abspath("mtXXXXXX", UPASTMP, file);
+        mktemp(s_to_c(file));
+        if((fd = syscreate(s_to_c(file), ORDWR|ORCLOSE, 0600))<0){
+                s_free(file);
+                return -1;
+        }
+        mp->tmp = file;
+
+        /*
+         *  read the rest into the temp file
+         */
+        while((n = Bread(fp, buf, sizeof(buf))) > 0){
+                if(write(fd, buf, n) != n){
+                        close(fd);
+                        return -1;
+                }
+                mp->size += n;
+                if(mp->size > MSGLIMIT){
+                        mp->size = -1;
+                        break;
+                }
+        }
+
+        mp->fd = fd;
+        return 0;
+}
+
+/* get the first address from a node */
+static String*
+getaddr(Node *p)
+{
+        for(; p; p = p->next)
+                if(p->s && p->addr)
+                        return s_copy(s_to_c(p->s));
+}
+
+/* get the text of a header line minus the field name */
+static String*
+getstring(Node *p)
+{
+        String *s;
+
+        s = s_new();
+        if(p == nil)
+                return s;
+
+        for(p = p->next; p; p = p->next){
+                if(p->s){
+                        s_append(s, s_to_c(p->s));
+                }else{
+                        s_putc(s, p->c);
+                        s_terminate(s);
+                }
+                if(p->white)
+                        s_append(s, s_to_c(p->white));
+        }
+        return s;
+}
+
+#if 0 /* jpc */
+static char *fieldname[] =
+{
+[WORD-WORD]        "WORD",
+[DATE-WORD]        "DATE",
+[RESENT_DATE-WORD]        "RESENT_DATE",
+[RETURN_PATH-WORD]        "RETURN_PATH",
+[FROM-WORD]        "FROM",
+[SENDER-WORD]        "SENDER",
+[REPLY_TO-WORD]        "REPLY_TO",
+[RESENT_FROM-WORD]        "RESENT_FROM",
+[RESENT_SENDER-WORD]        "RESENT_SENDER",
+[RESENT_REPLY_TO-WORD]        "RESENT_REPLY_TO",
+[SUBJECT-WORD]        "SUBJECT",
+[TO-WORD]        "TO",
+[CC-WORD]        "CC",
+[BCC-WORD]        "BCC",
+[RESENT_TO-WORD]        "RESENT_TO",
+[RESENT_CC-WORD]        "RESENT_CC",
+[RESENT_BCC-WORD]        "RESENT_BCC",
+[REMOTE-WORD]        "REMOTE",
+[PRECEDENCE-WORD]        "PRECEDENCE",
+[MIMEVERSION-WORD]        "MIMEVERSION",
+[CONTENTTYPE-WORD]        "CONTENTTYPE",
+[MESSAGEID-WORD]        "MESSAGEID",
+[RECEIVED-WORD]        "RECEIVED",
+[MAILER-WORD]        "MAILER",
+[BADTOKEN-WORD]        "BADTOKEN",
+};
+#endif /* jpc */
+
+/* fix 822 addresses */
+static void
+rfc822cruft(message *mp)
+{
+        Field *f;
+        Node *p;
+        String *body, *s;
+        char *cp;
+
+        /*
+         *  parse headers in in-core part
+         */
+        yyinit(s_to_c(mp->body), s_len(mp->body));
+        mp->rfc822headers = 0;
+        yyparse();
+        mp->rfc822headers = 1;
+        mp->received = received;
+
+        /*
+         *  remove equivalent systems in all addresses
+         */
+        body = s_new();
+        cp = s_to_c(mp->body);
+        for(f = firstfield; f; f = f->next){
+                if(f->node->c == MIMEVERSION)
+                        mp->havemime = 1;
+                if(f->node->c == FROM)
+                        mp->havefrom = getaddr(f->node);
+                if(f->node->c == SENDER)
+                        mp->havesender = getaddr(f->node);
+                if(f->node->c == REPLY_TO)
+                        mp->havereplyto = getaddr(f->node);
+                if(f->node->c == TO)
+                        mp->haveto = 1;
+                if(f->node->c == DATE)
+                        mp->havedate = 1;
+                if(f->node->c == SUBJECT)
+                        mp->havesubject = getstring(f->node);
+                if(f->node->c == PRECEDENCE && f->node->next && f->node->next->next){
+                        s = f->node->next->next->s;
+                        if(s && (strcmp(s_to_c(s), "bulk") == 0
+                                || strcmp(s_to_c(s), "Bulk") == 0))
+                                        mp->bulk = 1;
+                }
+                for(p = f->node; p; p = p->next){
+                        if(p->s){
+                                if(p->addr){
+                                        cp = skipequiv(s_to_c(p->s));
+                                        s_append(body, cp);
+                                } else 
+                                        s_append(body, s_to_c(p->s));
+                        }else{
+                                s_putc(body, p->c);
+                                s_terminate(body);
+                        }
+                        if(p->white)
+                                s_append(body, s_to_c(p->white));
+                        cp = p->end+1;
+                }
+                s_append(body, "\n");
+        }
+
+        if(*s_to_c(body) == 0){
+                s_free(body);
+                return;
+        }
+
+        if(*cp != '\n')
+                s_append(body, "\n");
+        s_memappend(body, cp, s_len(mp->body) - (cp - s_to_c(mp->body)));
+        s_terminate(body);
+
+        firstfield = 0;
+        mp->size += s_len(body) - s_len(mp->body);
+        s_free(mp->body);
+        mp->body = body;
+}
+
+/* read in a message, interpret the 'From' header */
+extern message *
+m_read(Biobuf *fp, int rmail, int interactive)
+{
+        message *mp;
+        Resub subexp[10];
+        char *line;
+        int first;
+        int n;
+
+        mp = m_new();
+
+        /* parse From lines if remote */
+        if (rmail) {
+                /* get remote address */
+                String *sender=s_new();
+
+                if (rfprog == 0)
+                        rfprog = regcomp(REMFROMRE);
+                first = 1;
+                while(s_read_line(fp, s_restart(mp->body)) != 0) {
+                        memset(subexp, 0, sizeof(subexp));
+                        if (regexec(rfprog, s_to_c(mp->body), subexp, 10) == 0){
+                                if(first == 0)
+                                        break;
+                                if (fprog == 0)
+                                        fprog = regcomp(FROMRE);
+                                memset(subexp, 0, sizeof(subexp));
+                                if(regexec(fprog, s_to_c(mp->body), subexp,10) == 0)
+                                        break;
+                                s_restart(mp->body);
+                                append_match(subexp, s_restart(sender), SENDERMATCH);
+                                append_match(subexp, s_restart(mp->date), DATEMATCH);
+                                break;
+                        }
+                        append_match(subexp, s_restart(sender), REMSENDERMATCH);
+                        append_match(subexp, s_restart(mp->date), REMDATEMATCH);
+                        if(subexp[REMSYSMATCH].s.sp!=subexp[REMSYSMATCH].e.ep){
+                                append_match(subexp, mp->sender, REMSYSMATCH);
+                                s_append(mp->sender, "!");
+                        }
+                        first = 0;
+                }
+                s_append(mp->sender, s_to_c(sender));
+
+                s_free(sender);
+        }
+        if(*s_to_c(mp->sender)=='\0')
+                default_from(mp);
+
+        /* if sender address is unreturnable, treat message as bulk mail */
+        if(!returnable(s_to_c(mp->sender)))
+                mp->bulk = 1;
+
+        /* get body */
+        if(interactive && !rmail){
+                /* user typing on terminal: terminator == '.' or EOF */
+                for(;;) {
+                        line = s_read_line(fp, mp->body);
+                        if (line == 0)
+                                break;
+                        if (strcmp(".\n", line)==0) {
+                                mp->body->ptr -= 2;
+                                *mp->body->ptr = '\0';
+                                break;
+                        }
+                }
+                mp->size = mp->body->ptr - mp->body->base;
+        } else {
+                /*
+                 *  read up to VMLIMIT bytes (more or less) into main memory.
+                 *  if message is longer put the rest in a tmp file.
+                 */
+                mp->size = mp->body->ptr - mp->body->base;
+                n = s_read(fp, mp->body, VMLIMIT);
+                if(n < 0){
+                        perror("m_read");
+                        exit(1);
+                }
+                mp->size += n;
+                if(n == VMLIMIT){
+                        if(m_read_to_file(fp, mp) < 0){
+                                perror("m_read");
+                                exit(1);
+                        }
+                }
+
+        }
+
+        /*
+         *  ignore 0 length messages from a terminal
+         */
+        if (!rmail && mp->size == 0)
+                return 0;
+
+        rfc822cruft(mp);
+
+        return mp;
+}
+
+/* return a piece of message starting at `offset' */
+extern int
+m_get(message *mp, long offset, char **pp)
+{
+        static char buf[4*1024];
+
+        /*
+         *  are we past eof?
+         */
+        if(offset >= mp->size)
+                return 0;
+
+        /*
+         *  are we in the virtual memory portion?
+         */
+        if(offset < s_len(mp->body)){
+                *pp = mp->body->base + offset;
+                return mp->body->ptr - mp->body->base - offset;
+        }
+
+        /*
+         *  read it from the temp file
+         */
+        offset -= s_len(mp->body);
+        if(mp->fd < 0)
+                return -1;
+        if(seek(mp->fd, offset, 0)<0)
+                return -1;
+        *pp = buf;
+        return read(mp->fd, buf, sizeof buf);
+}
+
+/* output the message body without ^From escapes */
+static int
+m_noescape(message *mp, Biobuf *fp)
+{
+        long offset;
+        int n;
+        char *p;
+
+        for(offset = 0; offset < mp->size; offset += n){
+                n = m_get(mp, offset, &p);
+                if(n <= 0){
+                        Bflush(fp);
+                        return -1;
+                }
+                if(Bwrite(fp, p, n) < 0)
+                        return -1;
+        }
+        return Bflush(fp);
+}
+
+/*
+ *  Output the message body with '^From ' escapes.
+ *  Ensures that any line starting with a 'From ' gets a ' ' stuck
+ *  in front of it.
+ */
+static int
+m_escape(message *mp, Biobuf *fp)
+{
+        char *p, *np;
+        char *end;
+        long offset;
+        int m, n;
+        char *start;
+
+        for(offset = 0; offset < mp->size; offset += n){
+                n = m_get(mp, offset, &start);
+                if(n < 0){
+                        Bflush(fp);
+                        return -1;
+                }
+
+                p = start;
+                for(end = p+n; p < end; p += m){
+                        np = memchr(p, '\n', end-p);
+                        if(np == 0){
+                                Bwrite(fp, p, end-p);
+                                break;
+                        }
+                        m = np - p + 1;
+                        if(m > 5 && strncmp(p, "From ", 5) == 0)
+                                Bputc(fp, ' ');
+                        Bwrite(fp, p, m);
+                }
+        }
+        Bflush(fp);
+        return 0;
+}
+
+static int
+printfrom(message *mp, Biobuf *fp)
+{
+        String *s;
+        int rv;
+
+        if(!returnable(s_to_c(mp->sender)))
+                return Bprint(fp, "From: Postmaster\n");
+
+        s = username(mp->sender);
+        if(s) {
+                s_append(s, " <");
+                s_append(s, s_to_c(mp->sender));
+                s_append(s, ">");
+        } else {
+                s = s_copy(s_to_c(mp->sender));
+        }
+        s = unescapespecial(s);
+        rv = Bprint(fp, "From: %s\n", s_to_c(s));
+        s_free(s);
+        return rv;
+}
+
+static 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;
+}
+
+int
+isutf8(String *s)
+{
+        char *p;
+        
+        for(p = s_to_c(s);  *p; p++)
+                if(*p&0x80)
+                        return 1;
+        return 0;
+}
+
+void
+printutf8mime(Biobuf *b)
+{
+        Bprint(b, "MIME-Version: 1.0\n");
+        Bprint(b, "Content-Type: text/plain; charset=\"UTF-8\"\n");
+        Bprint(b, "Content-Transfer-Encoding: 8bit\n");
+}
+
+/* output a message */
+extern int
+m_print(message *mp, Biobuf *fp, char *remote, int mbox)
+{
+        String *date, *sender;
+        char *f[6];
+        int n;
+
+        sender = unescapespecial(s_clone(mp->sender));
+
+        if (remote != 0){
+                if(print_remote_header(fp,s_to_c(sender),s_to_c(mp->date),remote) < 0){
+                        s_free(sender);
+                        return -1;
+                }
+        } else {
+                if(print_header(fp, s_to_c(sender), s_to_c(mp->date)) < 0){
+                        s_free(sender);
+                        return -1;
+                }
+        }
+        s_free(sender);
+        if(!rmail && !mp->havedate){
+                /* add a date: line Date: Sun, 19 Apr 1998 12:27:52 -0400 */
+                date = s_copy(s_to_c(mp->date));
+                n = getfields(s_to_c(date), f, 6, 1, " \t");
+                if(n == 6)
+                        Bprint(fp, "Date: %s, %s %s %s %s %s\n", f[0], f[2], f[1],
+                         f[5], f[3], rewritezone(f[4]));
+        }
+        if(!rmail && !mp->havemime && isutf8(mp->body))
+                printutf8mime(fp);
+        if(mp->to){
+                /* add the to: line */
+                if (Bprint(fp, "%s\n", s_to_c(mp->to)) < 0)
+                        return -1;
+                /* add the from: line */
+                if (!mp->havefrom && printfrom(mp, fp) < 0)
+                        return -1;
+                if(!mp->rfc822headers && *s_to_c(mp->body) != '\n')
+                        if (Bprint(fp, "\n") < 0)
+                                return -1;
+        } else if(!rmail){
+                /* add the from: line */
+                if (!mp->havefrom && printfrom(mp, fp) < 0)
+                        return -1;
+                if(!mp->rfc822headers && *s_to_c(mp->body) != '\n')
+                        if (Bprint(fp, "\n") < 0)
+                                return -1;
+        }
+
+        if (!mbox)
+                return m_noescape(mp, fp);
+        return m_escape(mp, fp);
+}
+
+/* print just the message body */
+extern int
+m_bprint(message *mp, Biobuf *fp)
+{
+        return m_noescape(mp, fp);
+}
diff --git a/src/cmd/upas/send/mkfile b/src/cmd/upas/send/mkfile
t@@ -0,0 +1,52 @@
+<$PLAN9/src/mkhdr
+
+TARG=send\
+        filter
+
+UOFILES=message.$O\
+        dest.$O\
+        log.$O\
+        skipequiv.$O\
+
+OFILES=\
+        $UOFILES\
+        ../smtp/rfc822.tab.$O\
+
+SMOBJ=main.$O\
+        bind.$O\
+        rewrite.$O\
+        local.$O\
+        translate.$O\
+        authorize.$O\
+        gateway.$O\
+        cat_mail.$O\
+
+LIB=../common/libcommon.av\
+
+HFILES=send.h\
+        ../common/common.h\
+        ../common/sys.h\
+
+LIB=../common/libcommon.a\
+
+BIN=$PLAN9/bin/upas
+UPDATE=\
+        mkfile\
+        $HFILES\
+        ${UOFILES:%.$O=%.c}\
+        ${SMOBJ:%.$O=%.c}\
+        ${TARG:%=%.c}\
+
+<$PLAN9/src/mkmany
+CFLAGS=$CFLAGS -I../common
+
+$O.send: $SMOBJ $OFILES
+        $LD $LDFLAGS -o $target $prereq $LIB
+
+message.$O:        ../smtp/y.tab.h
+
+../smtp/y.tab.h ../smtp/rfc822.tab.$O: ../smtp/rfc822.y
+#        @{
+                cd ../smtp
+                mk rfc822.tab.$O
+#        }
diff --git a/src/cmd/upas/send/regtest.c b/src/cmd/upas/send/regtest.c
t@@ -0,0 +1,36 @@
+#include 
+#include 
+#include 
+#include 
+
+main(void)
+{
+        char *re;
+        char *line;
+        Reprog *prog;
+        char *cp;
+        Biobuf in;
+
+        Binit(&in, 0, OREAD);
+        print("re> ");
+        while(re = Brdline(&in, '\n')){
+                re[Blinelen(&in)-1] = 0;
+                if(*re == 0)
+                        break;
+                prog = regcomp(re);
+                print("> ");
+                while(line = Brdline(&in, '\n')){
+                        line[Blinelen(&in)-1] = 0;
+                        if(cp = strchr(line, '\n'))
+                                *cp = 0;
+                        if(*line == 0)
+                                break;
+                        if(regexec(prog, line, 0))
+                                print("yes\n");
+                        else
+                                print("no\n");
+                        print("> ");
+                }
+                print("re> ");
+        }
+}
diff --git a/src/cmd/upas/send/rewrite.c b/src/cmd/upas/send/rewrite.c
t@@ -0,0 +1,315 @@
+#include "common.h"
+#include "send.h"
+
+extern int debug;
+
+/* 
+ *        Routines for dealing with the rewrite rules.
+ */
+
+/* globals */
+typedef struct rule rule;
+
+#define NSUBEXP 10
+struct rule {
+        String *matchre;        /* address match */
+        String *repl1;                /* first replacement String */
+        String *repl2;                /* second replacement String */
+        d_status type;                /* type of rule */
+        Reprog *program;
+        Resub subexp[NSUBEXP];
+        rule *next;
+};
+static rule *rulep;
+static rule *rlastp;
+
+/* predeclared */
+static String *substitute(String *, Resub *, message *);
+static rule *findrule(String *, int);
+
+
+/*
+ *  Get the next token from `line'.  The symbol `\l' is replaced by
+ *  the name of the local system.
+ */
+extern String *
+rule_parse(String *line, char *system, int *backl)
+{
+        String *token;
+        String *expanded;
+        char *cp;
+
+        token = s_parse(line, 0);
+        if(token == 0)
+                return(token);
+        if(strchr(s_to_c(token), '\\')==0)
+                return(token);
+        expanded = s_new();
+        for(cp = s_to_c(token); *cp; cp++) {
+                if(*cp == '\\') switch(*++cp) {
+                case 'l':
+                        s_append(expanded, system);
+                        *backl = 1;
+                        break;
+                case '\\':
+                        s_putc(expanded, '\\');
+                        break;
+                default:
+                        s_putc(expanded, '\\');
+                        s_putc(expanded, *cp);
+                        break;
+                } else
+                        s_putc(expanded, *cp);
+        }
+        s_free(token);
+        s_terminate(expanded);
+        return(expanded);
+}
+
+static int
+getrule(String *line, String *type, char *system)
+{
+        rule        *rp;
+        String        *re;
+        int        backl;
+
+        backl = 0;
+
+        /* get a rule */
+        re = rule_parse(s_restart(line), system, &backl);
+        if(re == 0)
+                return 0;
+        rp = (rule *)malloc(sizeof(rule));
+        if(rp == 0) {
+                perror("getrules:");
+                exit(1);
+        }
+        rp->next = 0;
+        s_tolower(re);
+        rp->matchre = s_new();
+        s_append(rp->matchre, s_to_c(re));
+        s_restart(rp->matchre);
+        s_free(re);
+        s_parse(line, s_restart(type));
+        rp->repl1 = rule_parse(line, system, &backl);
+        rp->repl2 = rule_parse(line, system, &backl);
+        rp->program = 0;
+        if(strcmp(s_to_c(type), "|") == 0)
+                rp->type = d_pipe;
+        else if(strcmp(s_to_c(type), ">>") == 0)
+                rp->type = d_cat;
+        else if(strcmp(s_to_c(type), "alias") == 0)
+                rp->type = d_alias;
+        else if(strcmp(s_to_c(type), "translate") == 0)
+                rp->type = d_translate;
+        else if(strcmp(s_to_c(type), "auth") == 0)
+                rp->type = d_auth;
+        else {
+                s_free(rp->matchre);
+                s_free(rp->repl1);
+                s_free(rp->repl2);
+                free((char *)rp);
+                fprint(2,"illegal rewrite rule: %s\n", s_to_c(line));
+                return 0;
+        }
+        if(rulep == 0)
+                rulep = rlastp = rp;
+        else
+                rlastp = rlastp->next = rp;
+        return backl;
+}
+
+/*
+ *  rules are of the form:
+ *           []
+ */
+extern int
+getrules(void)
+{
+        Biobuf        *rfp;
+        String        *line;
+        String        *type;
+        String        *file;
+
+        file = abspath("rewrite", unsharp(UPASLIB), (String *)0);
+        rfp = sysopen(s_to_c(file), "r", 0);
+        if(rfp == 0) {
+                rulep = 0;
+                return -1;
+        }
+        rlastp = 0;
+        line = s_new();
+        type = s_new();
+        while(s_getline(rfp, s_restart(line)))
+                if(getrule(line, type, thissys) && altthissys)
+                        getrule(s_restart(line), type, altthissys);
+        s_free(type);
+        s_free(line);
+        s_free(file);
+        sysclose(rfp);
+        return 0;
+}
+
+/* look up a matching rule */
+static rule *
+findrule(String *addrp, int authorized)
+{
+        rule *rp;
+        static rule defaultrule;
+
+        if(rulep == 0)
+                return &defaultrule;
+        for (rp = rulep; rp != 0; rp = rp->next) {
+                if(rp->type==d_auth && authorized)
+                        continue;
+                if(rp->program == 0)
+                        rp->program = regcomp(rp->matchre->base);
+                if(rp->program == 0)
+                        continue;
+                memset(rp->subexp, 0, sizeof(rp->subexp));
+                if(debug)
+                        print("matching %s aginst %s\n", s_to_c(addrp), rp->matchre->base);
+                if(regexec(rp->program, s_to_c(addrp), rp->subexp, NSUBEXP))
+                if(s_to_c(addrp) == rp->subexp[0].s.sp)
+                if((s_to_c(addrp) + strlen(s_to_c(addrp))) == rp->subexp[0].e.ep)
+                        return rp;
+        }
+        return 0;
+}
+
+/*  Transforms the address into a command.
+ *  Returns:        -1 ifaddress not matched by reules
+ *                 0 ifaddress matched and ok to forward
+ *                 1 ifaddress matched and not ok to forward
+ */
+extern int
+rewrite(dest *dp, message *mp)
+{
+        rule *rp;                /* rewriting rule */
+        String *lower;                /* lower case version of destination */
+
+        /*
+         *  Rewrite the address.  Matching is case insensitive.
+         */
+        lower = s_clone(dp->addr);
+        s_tolower(s_restart(lower));
+        rp = findrule(lower, dp->authorized);
+        if(rp == 0){
+                s_free(lower);
+                return -1;
+        }
+        strcpy(s_to_c(lower), s_to_c(dp->addr));
+        dp->repl1 = substitute(rp->repl1, rp->subexp, mp);
+        dp->repl2 = substitute(rp->repl2, rp->subexp, mp);
+        dp->status = rp->type;
+        if(debug){
+                print("\t->");
+                if(dp->repl1)
+                        print("%s", s_to_c(dp->repl1));
+                if(dp->repl2)
+                        print("%s", s_to_c(dp->repl2));
+                print("\n");
+        }
+        s_free(lower);
+        return 0;
+}
+
+static String *
+substitute(String *source, Resub *subexp, message *mp)
+{
+        int i;
+        char *s;
+        char *sp;
+        String *stp;
+        
+        if(source == 0)
+                return 0;
+        sp = s_to_c(source);
+
+        /* someplace to put it */
+        stp = s_new();
+
+        /* do the substitution */
+        while (*sp != '\0') {
+                if(*sp == '\\') {
+                        switch (*++sp) {
+                        case '0': case '1': case '2': case '3': case '4':
+                        case '5': case '6': case '7': case '8': case '9':
+                                i = *sp-'0';
+                                if(subexp[i].s.sp != 0)
+                                        for (s = subexp[i].s.sp;
+                                             s < subexp[i].e.ep;
+                                             s++)
+                                                s_putc(stp, *s);
+                                break;
+                        case '\\':
+                                s_putc(stp, '\\');
+                                break;
+                        case '\0':
+                                sp--;
+                                break;
+                        case 's':
+                                for(s = s_to_c(mp->replyaddr); *s; s++)
+                                        s_putc(stp, *s);
+                                break;
+                        case 'p':
+                                if(mp->bulk)
+                                        s = "bulk";
+                                else
+                                        s = "normal";
+                                for(;*s; s++)
+                                        s_putc(stp, *s);
+                                break;
+                        default:
+                                s_putc(stp, *sp);
+                                break;
+                        }
+                } else if(*sp == '&') {                                
+                        if(subexp[0].s.sp != 0)
+                                for (s = subexp[0].s.sp;
+                                     s < subexp[0].e.ep; s++)
+                                        s_putc(stp, *s);
+                } else
+                        s_putc(stp, *sp);
+                sp++;
+        }
+        s_terminate(stp);
+
+        return s_restart(stp);
+}
+
+extern void
+regerror(char* s)
+{
+        fprint(2, "rewrite: %s\n", s);
+}
+
+extern void
+dumprules(void)
+{
+        rule *rp;
+
+        for (rp = rulep; rp != 0; rp = rp->next) {
+                fprint(2, "'%s'", rp->matchre->base);
+                switch (rp->type) {
+                case d_pipe:
+                        fprint(2, " |");
+                        break;
+                case d_cat:
+                        fprint(2, " >>");
+                        break;
+                case d_alias:
+                        fprint(2, " alias");
+                        break;
+                case d_translate:
+                        fprint(2, " translate");
+                        break;
+                default:
+                        fprint(2, " UNKNOWN");
+                        break;
+                }
+                fprint(2, " '%s'", rp->repl1 ? rp->repl1->base:"...");
+                fprint(2, " '%s'\n", rp->repl2 ? rp->repl2->base:"...");
+        }
+}
+
diff --git a/src/cmd/upas/send/send.h b/src/cmd/upas/send/send.h
t@@ -0,0 +1,108 @@
+#define MAXSAME 16
+#define MAXSAMECHAR 1024
+
+/* status of a destination*/
+typedef enum {
+        d_undefined,        /* address has not been matched*/
+        d_pipe,                /* repl1|repl2 == delivery command, rep*/
+        d_cat,                /* repl1 == mail file */
+        d_translate,        /* repl1 == translation command*/
+        d_alias,        /* repl1 == translation*/
+        d_auth,                /* repl1 == command to authorize*/
+        d_syntax,        /* addr contains illegal characters*/
+        d_unknown,        /* addr does not match a rewrite rule*/
+        d_loop,                /* addressing loop*/
+        d_eloop,        /* external addressing loop*/
+        d_noforward,        /* forwarding not allowed*/
+        d_badmbox,        /* mailbox badly formatted*/
+        d_resource,        /* ran out of something we needed*/
+        d_pipeto,        /* pipe to from a mailbox*/
+} d_status;
+
+/* a destination*/
+typedef struct dest dest;
+struct dest {
+        dest        *next;                /* for chaining*/
+        dest        *same;                /* dests with same cmd*/
+        dest        *parent;        /* destination we're a translation of*/
+        String        *addr;                /* destination address*/
+        String        *repl1;                /* substitution field 1*/
+        String        *repl2;                /* substitution field 2*/
+        int        pstat;                /* process status*/
+        d_status status;        /* delivery status*/
+        int        authorized;        /* non-zero if we have been authorized*/
+        int        nsame;                /* number of same dests chained to this entry*/
+        int        nchar;                /* number of characters in the command*/
+};
+
+typedef struct message message;
+struct message {
+        String        *sender;
+        String        *replyaddr;
+        String        *date;
+        String        *body;
+        String        *tmp;                /* name of temp file */
+        String        *to;
+        int        size;
+        int        fd;                /* if >= 0, the file the message is stored in*/
+        char        haveto;
+        String        *havefrom;
+        String        *havesender;
+        String        *havereplyto;
+        char        havedate;
+        char        havemime;
+        String        *havesubject;
+        char        bulk;                /* if Precedence: Bulk in header */
+        char        rfc822headers;
+        int        received;        /* number of received lines */
+        char        *boundary;        /* bondary marker for attachments */
+};
+
+/*
+ *  exported variables
+ */
+extern int rmail;
+extern int onatty;
+extern char *thissys, *altthissys;
+extern int xflg;
+extern int nflg;
+extern int tflg;
+extern int debug;
+extern int nosummary;
+
+/*
+ *  exported procedures
+ */
+extern void        authorize(dest*);
+extern int        cat_mail(dest*, message*);
+extern dest        *up_bind(dest*, message*, int);
+extern int        ok_to_forward(char*);
+extern int        lookup(char*, char*, Biobuf**, char*, Biobuf**);
+extern dest        *d_new(String*);
+extern void        d_free(dest*);
+extern dest        *d_rm(dest**);
+extern void        d_insert(dest**, dest*);
+extern dest        *d_rm_same(dest**);
+extern void        d_same_insert(dest**, dest*);
+extern String        *d_to(dest*);
+extern dest        *s_to_dest(String*, dest*);
+extern void        gateway(message*);
+extern dest        *expand_local(dest*);
+extern void        logdelivery(dest*, char*, message*);
+extern void        loglist(dest*, message*, char*);
+extern void        logrefusal(dest*, message*, char*);
+extern int        default_from(message*);
+extern message        *m_new(void);
+extern void        m_free(message*);
+extern message        *m_read(Biobuf*, int, int);
+extern int        m_get(message*, long, char**);
+extern int        m_print(message*, Biobuf*, char*, int);
+extern int        m_bprint(message*, Biobuf*);
+extern String        *rule_parse(String*, char*, int*);
+extern int        getrules(void);
+extern int        rewrite(dest*, message*);
+extern void        dumprules(void);
+extern void        regerror(char*);
+extern dest        *translate(dest*);
+extern char*        skipequiv(char*);
+extern int        refuse(dest*, message*, char*, int, int);
diff --git a/src/cmd/upas/send/skipequiv.c b/src/cmd/upas/send/skipequiv.c
t@@ -0,0 +1,93 @@
+#include "common.h"
+#include "send.h"
+
+#undef isspace
+#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
+
+/*
+ *  skip past all systems in equivlist
+ */
+extern char*
+skipequiv(char *base)
+{
+        char *sp;
+        static Biobuf *fp;
+
+        while(*base){
+                sp = strchr(base, '!');
+                if(sp==0)
+                        break;
+                *sp = '\0';
+                if(lookup(base, "equivlist", &fp, 0, 0)==1){
+                        /* found or us, forget this system */
+                        *sp='!';
+                        base=sp+1;
+                } else {
+                        /* no files or system is not found, and not us */
+                        *sp='!';
+                        break;
+                }
+        }
+        return base;
+}
+
+static int
+okfile(char *cp, Biobuf *fp)
+{
+        char *buf;
+        int len;
+        char *bp, *ep;
+        int c;
+
+        len = strlen(cp);
+        Bseek(fp, 0, 0);
+        
+        /* one iteration per system name in the file */
+        while(buf = Brdline(fp, '\n')) {
+                ep = &buf[Blinelen(fp)];
+                for(bp=buf; bp < ep;){
+                        while(isspace(*bp) || *bp==',')
+                                bp++;
+                        if(strncmp(bp, cp, len) == 0) {
+                                c = *(bp+len);
+                                if(isspace(c) || c==',')
+                                        return 1;
+                        }
+                        while(bp < ep && (!isspace(*bp)) && *bp!=',')
+                                bp++;
+                }
+        }
+
+        /* didn't find it, prohibit forwarding */
+        return 0;
+}
+
+/* return 1 if name found in one of the files
+ *          0 if name not found in one of the files
+ *          -1 if neither file exists
+ */
+extern int
+lookup(char *cp, char *local, Biobuf **lfpp, char *global, Biobuf **gfpp)
+{
+        static String *file = 0;
+
+        if (local) {
+                if (file == 0)
+                        file = s_new();
+                abspath(local, UPASLIB, s_restart(file));
+                if (*lfpp != 0 || (*lfpp = sysopen(s_to_c(file), "r", 0)) != 0) {
+                        if (okfile(cp, *lfpp))
+                                return 1;
+                } else
+                        local = 0;
+        }
+        if (global) {
+                abspath(global, UPASLIB, s_restart(file));
+                if (*gfpp != 0 || (*gfpp = sysopen(s_to_c(file), "r", 0)) != 0) {
+                        if (okfile(cp, *gfpp))
+                                return 1;
+                } else
+                        global = 0;
+        }
+        return (local || global)? 0 : -1;
+}
diff --git a/src/cmd/upas/send/translate.c b/src/cmd/upas/send/translate.c
t@@ -0,0 +1,43 @@
+#include "common.h"
+#include "send.h"
+
+/* pipe an address through a command to translate it */
+extern dest *
+translate(dest *dp)
+{
+        process *pp;
+        String *line;
+        dest *rv;
+        char *cp;
+        int n;
+
+        pp = proc_start(s_to_c(dp->repl1), (stream *)0, outstream(), outstream(), 1, 0);
+        if (pp == 0) {
+                dp->status = d_resource;
+                return 0;
+        }
+        line = s_new();
+        for(;;) {
+                cp = Brdline(pp->std[1]->fp, '\n');
+                if(cp == 0)
+                        break;
+                if(strncmp(cp, "_nosummary_", 11) == 0){
+                        nosummary = 1;
+                        continue;
+                }
+                n = Blinelen(pp->std[1]->fp);
+                cp[n-1] = ' ';
+                s_nappend(line, cp, n);
+        }
+        rv = s_to_dest(s_restart(line), dp);
+        s_restart(line);
+        while(s_read_line(pp->std[2]->fp, line))
+                ;
+        if ((dp->pstat = proc_wait(pp)) != 0) {
+                dp->repl2 = line;
+                rv = 0;
+        } else
+                s_free(line);
+        proc_free(pp);
+        return rv;
+}
diff --git a/src/cmd/upas/send/tryit b/src/cmd/upas/send/tryit
t@@ -0,0 +1,29 @@
+#!/bin/sh
+set -x
+
+> /usr/spool/mail/test.local
+echo "Forward to test.local" > /usr/spool/mail/test.forward
+echo "Pipe to cat > /tmp/test.mail" > /usr/spool/mail/test.pipe
+chmod 644 /usr/spool/mail/test.pipe
+
+mail test.local <>>test.local<<<"
+cat /usr/spool/mail/test.local
+echo ">>>test.mail<<<"
+cat /tmp/test.mail
diff --git a/src/cmd/upas/smtp/greylist.c b/src/cmd/upas/smtp/greylist.c
t@@ -0,0 +1,274 @@
+#include "common.h"
+#include "smtpd.h"
+#include "smtp.h"
+#include 
+#include 
+#include 
+
+typedef struct {
+        int        existed;        /* these two are distinct to cope with errors */
+        int        created;
+        int        noperm;
+        long        mtime;                /* mod time, iff it already existed */
+} Greysts;
+
+/*
+ * There's a bit of a problem with yahoo; they apparently have a vast
+ * pool of machines that all run the same queue(s), so a 451 retry can
+ * come from a different IP address for many, many retries, and it can
+ * take ~5 hours for the same IP to call us back.  Various other goofballs,
+ * notably the IEEE, try to send mail just before 9 AM, then refuse to try
+ * again until after 5 PM.  Doh!
+ */
+enum {
+        Nonspammax = 14*60*60,  /* must call back within this time if real */
+};
+static char whitelist[] = "/mail/lib/whitelist";
+
+/*
+ * matches ip addresses or subnets in whitelist against nci->rsys.
+ * ignores comments and blank lines in /mail/lib/whitelist.
+ */
+static int
+onwhitelist(void)
+{
+        int lnlen;
+        char *line, *parse;
+        char input[128];
+        uchar ip[IPaddrlen], ipmasked[IPaddrlen];
+        uchar mask4[IPaddrlen], addr4[IPaddrlen];
+        uchar mask[IPaddrlen], addr[IPaddrlen], addrmasked[IPaddrlen];
+        Biobuf *wl;
+        static int beenhere;
+        static allzero[IPaddrlen];
+
+        if (!beenhere) {
+                beenhere = 1;
+                fmtinstall('I', eipfmt);
+        }
+
+        parseip(ip, nci->rsys);
+        wl = Bopen(whitelist, OREAD);
+        if (wl == nil)
+                return 1;
+        while ((line = Brdline(wl, '\n')) != nil) {
+                if (line[0] == '#' || line[0] == '\n')
+                        continue;
+                lnlen = Blinelen(wl);
+                line[lnlen-1] = '\0';                /* clobber newline */
+
+                /* default mask is /32 (v4) or /128 (v6) for bare IP */
+                parse = line;
+                if (strchr(line, '/') == nil) {
+                        strncpy(input, line, sizeof input - 5);
+                        if (strchr(line, '.') != nil)
+                                strcat(input, "/32");
+                        else
+                                strcat(input, "/128");
+                        parse = input;
+                }
+                /* sorry, dave; where's parsecidr for v4 or v6? */
+                v4parsecidr(addr4, mask4, parse);
+                v4tov6(addr, addr4);
+                v4tov6(mask, mask4);
+
+                maskip(addr, mask, addrmasked);
+                maskip(ip, mask, ipmasked);
+                if (memcmp(ipmasked, addrmasked, IPaddrlen) == 0)
+                        break;
+        }
+        Bterm(wl);
+        return line != nil;
+}
+
+static int mkdirs(char *);
+
+/*
+ * if any directories leading up to path don't exist, create them.
+ * modifies but restores path.
+ */
+static int
+mkpdirs(char *path)
+{
+        int rv = 0;
+        char *sl = strrchr(path, '/');
+
+        if (sl != nil) {
+                *sl = '\0';
+                rv = mkdirs(path);
+                *sl = '/';
+        }
+        return rv;
+}
+
+/*
+ * if path or any directories leading up to it don't exist, create them.
+ * modifies but restores path.
+ */
+static int
+mkdirs(char *path)
+{
+        int fd;
+
+        if (access(path, AEXIST) >= 0)
+                return 0;
+
+        /* make presumed-missing intermediate directories */
+        if (mkpdirs(path) < 0)
+                return -1;
+
+        /* make final directory */
+        fd = create(path, OREAD, 0777|DMDIR);
+        if (fd < 0)
+                /*
+                 * we may have lost a race; if the directory now exists,
+                 * it's okay.
+                 */
+                return access(path, AEXIST) < 0? -1: 0;
+        close(fd);
+        return 0;
+}
+
+static long
+getmtime(char *file)
+{
+        long mtime = -1;
+        Dir *ds = dirstat(file);
+
+        if (ds != nil) {
+                mtime = ds->mtime;
+                free(ds);
+        }
+        return mtime;
+}
+
+static void
+tryaddgrey(char *file, Greysts *gsp)
+{
+        int fd = create(file, OWRITE|OEXCL, 0444|DMEXCL);
+
+        gsp->created = (fd >= 0);
+        if (fd >= 0) {
+                close(fd);
+                gsp->existed = 0;  /* just created; couldn't have existed */
+        } else {
+                /*
+                 * why couldn't we create file? it must have existed
+                 * (or we were denied perm on parent dir.).
+                 * if it existed, fill in gsp->mtime; otherwise
+                 * make presumed-missing intermediate directories.
+                 */
+                gsp->existed = access(file, AEXIST) >= 0;
+                if (gsp->existed)
+                        gsp->mtime = getmtime(file);
+                else if (mkpdirs(file) < 0)
+                        gsp->noperm = 1;
+        }
+}
+
+static void
+addgreylist(char *file, Greysts *gsp)
+{
+        tryaddgrey(file, gsp);
+        if (!gsp->created && !gsp->existed && !gsp->noperm)
+                /* retry the greylist entry with parent dirs created */
+                tryaddgrey(file, gsp);
+}
+
+static int
+recentcall(Greysts *gsp)
+{
+        long delay = time(0) - gsp->mtime;
+
+        if (!gsp->existed)
+                return 0;
+        /* reject immediate call-back; spammers are doing that now */
+        return delay >= 30 && delay <= Nonspammax;
+}
+
+/*
+ * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
+ * reject this message as "451 temporary failure".  if the caller is real,
+ * he'll retry soon, otherwise he's a spammer.
+ * at the first rejection, create a greylist entry for (my-ip, caller-ip,
+ * rcpt, time), where time is the file's mtime.  if they call back and there's
+ * already a greylist entry, and it's within the allowed interval,
+ * add their IP to the append-only whitelist.
+ *
+ * greylist files can be removed at will; at worst they'll cause a few
+ * extra retries.
+ */
+
+static int
+isrcptrecent(char *rcpt)
+{
+        char *user;
+        char file[256];
+        Greysts gs;
+        Greysts *gsp = &gs;
+
+        if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
+            strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
+                return 0;
+
+        /* shorten names to fit pre-fossil or pre-9p2000 file servers */
+        user = strrchr(rcpt, '!');
+        if (user == nil)
+                user = rcpt;
+        else
+                user++;
+
+        /* check & try to update the grey list entry */
+        snprint(file, sizeof file, "/mail/grey/%s/%s/%s",
+                nci->lsys, nci->rsys, user);
+        memset(gsp, 0, sizeof *gsp);
+        addgreylist(file, gsp);
+
+        /* if on greylist already and prior call was recent, add to whitelist */
+        if (gsp->existed && recentcall(gsp)) {
+                syslog(0, "smtpd",
+                        "%s/%s was grey; adding IP to white", nci->rsys, rcpt);
+                return 1;
+        } else if (gsp->existed)
+                syslog(0, "smtpd", "call for %s/%s was seconds ago or long ago",
+                        nci->rsys, rcpt);
+        else
+                syslog(0, "smtpd", "no call registered for %s/%s; registering",
+                        nci->rsys, rcpt);
+        return 0;
+}
+
+void
+vfysenderhostok(void)
+{
+        char *fqdn;
+        int recent = 0;
+        Link *l;
+
+        if (onwhitelist())
+                return;
+
+        for (l = rcvers.first; l; l = l->next)
+                if (isrcptrecent(s_to_c(l->p)))
+                        recent = 1;
+
+        /* if on greylist already and prior call was recent, add to whitelist */
+        if (recent) {
+                int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
+
+                if (fd >= 0) {
+                        seek(fd, 0, 2);                        /* paranoia */
+                        if ((fqdn = csgetvalue(nil, "ip", nci->rsys, "dom", nil)) != nil)
+                                fprint(fd, "# %s\n%s\n\n", fqdn, nci->rsys);
+                        else
+                                fprint(fd, "# unknown\n%s\n\n", nci->rsys);
+                        close(fd);
+                }
+        } else {
+                syslog(0, "smtpd",
+        "no recent call from %s for a rcpt; rejecting with temporary failure",
+                        nci->rsys);
+                reply("451 please try again soon from the same IP.\r\n");
+                exits("no recent call for a rcpt");
+        }
+}
diff --git a/src/cmd/upas/smtp/mkfile b/src/cmd/upas/smtp/mkfile
t@@ -0,0 +1,54 @@
+<$PLAN9/src/mkhdr
+
+TARG = # smtpd\
+        smtp\
+
+OFILES=
+
+LIB=../common/libcommon.a\
+        $PLAN9/lib/libthread.a   # why do i have to explicitly put this?
+
+HFILES=../common/common.h\
+        ../common/sys.h\
+        smtpd.h\
+        smtp.h\
+
+BIN=$PLAN9/bin/upas
+UPDATE=\
+        greylist.c\
+        mkfile\
+        mxdial.c\
+        rfc822.y\
+        rmtdns.c\
+        smtpd.y\
+        spam.c\
+        $HFILES\
+        ${OFILES:%.$O=%.c}\
+        ${TARG:%=%.c}\
+
+<$PLAN9/src/mkmany
+CFLAGS=$CFLAGS -I../common -D'SPOOL="/mail"'
+
+$O.smtpd:        smtpd.tab.$O rmtdns.$O spam.$O rfc822.tab.$O greylist.$O
+$O.smtp:        rfc822.tab.$O mxdial.$O
+
+smtpd.$O:         smtpd.h
+
+smtp.$O to.$O:         smtp.h
+
+smtpd.tab.c: smtpd.y smtpd.h
+        yacc -o xxx smtpd.y
+        sed 's/yy/zz/g' < xxx > $target
+        rm xxx
+
+rfc822.tab.c: rfc822.y smtp.h
+        9 yacc -d -o $target rfc822.y
+
+clean:V:
+        rm -f *.[$OS] [$OS].$TARG smtpd.tab.c rfc822.tab.c y.tab.? y.debug $TARG
+
+../common/libcommon.a$O:
+        @{         
+                cd ../common
+                mk
+        }
diff --git a/src/cmd/upas/smtp/mxdial.c b/src/cmd/upas/smtp/mxdial.c
t@@ -0,0 +1,333 @@
+#include "common.h"
+#include 
+#include "smtp.h"        /* to publish dial_string_parse */
+
+enum
+{
+        Nmx=        16,
+        Maxstring=        256,
+};
+
+typedef struct Mx        Mx;
+struct Mx
+{
+        char host[256];
+        char ip[24];
+        int pref;
+};
+static Mx mx[Nmx];
+
+Ndb *db;
+extern int debug;
+
+static int        mxlookup(DS*, char*);
+static int        mxlookup1(DS*, char*);
+static int        compar(void*, void*);
+static int        callmx(DS*, char*, char*);
+static void expand_meta(DS *ds);
+extern int        cistrcmp(char*, char*);
+
+int
+mxdial(char *addr, char *ddomain, char *gdomain)
+{
+        int fd;
+        DS ds;
+        char err[Errlen];
+
+        addr = netmkaddr(addr, 0, "smtp");
+        dial_string_parse(addr, &ds);
+
+        /* try connecting to destination or any of it's mail routers */
+        fd = callmx(&ds, addr, ddomain);
+
+        /* try our mail gateway */
+        rerrstr(err, sizeof(err));
+        if(fd < 0 && gdomain && strstr(err, "can't translate") != 0) {
+                fprint(2,"dialing %s\n",gdomain);
+                fd = dial(netmkaddr(gdomain, 0, "smtp"), 0, 0, 0);
+        }
+
+        return fd;
+}
+
+/*
+ *  take an address and return all the mx entries for it,
+ *  most preferred first
+ */
+static int
+callmx(DS *ds, char *dest, char *domain)
+{
+        int fd, i, nmx;
+        char addr[Maxstring];
+
+        /* get a list of mx entries */
+        nmx = mxlookup(ds, domain);
+        if(nmx < 0){
+                /* dns isn't working, don't just dial */
+                return -1;
+        }
+        if(nmx == 0){
+                if(debug)
+                        fprint(2, "mxlookup returns nothing\n");
+                return dial(dest, 0, 0, 0);
+        }
+
+        /* refuse to honor loopback addresses given by dns */
+        for(i = 0; i < nmx; i++){
+                if(strcmp(mx[i].ip, "127.0.0.1") == 0){
+                        if(debug)
+                                fprint(2, "mxlookup returns loopback\n");
+                        werrstr("illegal: domain lists 127.0.0.1 as mail server");
+                        return -1;
+                }
+        }
+
+        /* sort by preference */
+        if(nmx > 1)
+                qsort(mx, nmx, sizeof(Mx), compar);
+
+        /* dial each one in turn */
+        for(i = 0; i < nmx; i++){
+                snprint(addr, sizeof(addr), "%s/%s!%s!%s", ds->netdir, ds->proto,
+                        mx[i].host, ds->service);
+                if(debug)
+                        fprint(2, "mxdial trying %s\n", addr);
+                fd = dial(addr, 0, 0, 0);
+                if(fd >= 0)
+                        return fd;
+        }
+        return -1;
+}
+
+/*
+ *  call the dns process and have it try to resolve the mx request
+ *
+ *  this routine knows about the firewall and tries inside and outside
+ *  dns's seperately.
+ */
+static int
+mxlookup(DS *ds, char *domain)
+{
+        int n;
+
+        /* just in case we find no domain name */
+        strcpy(domain, ds->host);
+
+        if(ds->netdir){
+                n = mxlookup1(ds, domain);
+        } else {
+                ds->netdir = "/net";
+                n = mxlookup1(ds, domain);
+                if(n == 0) {
+                        ds->netdir = "/net.alt";
+                        n = mxlookup1(ds, domain);
+                }
+        }
+
+        return n;
+}
+
+static int
+mxlookup1(DS *ds, char *domain)
+{
+        char buf[1024];
+        char dnsname[Maxstring];
+        char *fields[4];
+        int i, n, fd, nmx;
+
+        snprint(dnsname, sizeof dnsname, "%s/dns", ds->netdir);
+
+        fd = open(dnsname, ORDWR);
+        if(fd < 0)
+                return 0;
+
+        nmx = 0;
+        snprint(buf, sizeof(buf), "%s mx", ds->host);
+        if(debug)
+                fprint(2, "sending %s '%s'\n", dnsname, buf);
+        n = write(fd, buf, strlen(buf));
+        if(n < 0){
+                rerrstr(buf, sizeof buf);
+                if(debug)
+                        fprint(2, "dns: %s\n", buf);
+                if(strstr(buf, "dns failure")){
+                        /* if dns fails for the mx lookup, we have to stop */
+                        close(fd);
+                        return -1;
+                }
+        } else {
+                /*
+                 *  get any mx entries
+                 */
+                seek(fd, 0, 0);
+                while(nmx < Nmx && (n = read(fd, buf, sizeof(buf)-1)) > 0){
+                        buf[n] = 0;
+                        if(debug)
+                                fprint(2, "dns mx: %s\n", buf);
+                        n = getfields(buf, fields, 4, 1, " \t");
+                        if(n < 4)
+                                continue;
+
+                        if(strchr(domain, '.') == 0)
+                                strcpy(domain, fields[0]);
+
+                        strncpy(mx[nmx].host, fields[3], sizeof(mx[n].host)-1);
+                        mx[nmx].pref = atoi(fields[2]);
+                        nmx++;
+                }
+                if(debug)
+                        fprint(2, "dns mx; got %d entries\n", nmx);
+        }
+
+        /*
+         * no mx record? try name itself.
+         */
+        /*
+         * BUG? If domain has no dots, then we used to look up ds->host
+         * but return domain instead of ds->host in the list.  Now we return
+         * ds->host.  What will this break?
+         */
+        if(nmx == 0){
+                mx[0].pref = 1;
+                strncpy(mx[0].host, ds->host, sizeof(mx[0].host));
+                nmx++;
+        }
+
+        /*
+         * look up all ip addresses
+         */
+        for(i = 0; i < nmx; i++){
+                seek(fd, 0, 0);
+                snprint(buf, sizeof buf, "%s ip", mx[i].host);
+                mx[i].ip[0] = 0;
+                if(write(fd, buf, strlen(buf)) < 0)
+                        goto no;
+                seek(fd, 0, 0);
+                if((n = read(fd, buf, sizeof buf-1)) < 0)
+                        goto no;
+                buf[n] = 0;
+                if(getfields(buf, fields, 4, 1, " \t") < 3)
+                        goto no;
+                strncpy(mx[i].ip, fields[2], sizeof(mx[i].ip)-1);
+                continue;
+
+        no:
+                /* remove mx[i] and go around again */
+                nmx--;
+                mx[i] = mx[nmx];
+                i--;
+        }
+        return nmx;                
+}
+
+static int
+compar(void *a, void *b)
+{
+        return ((Mx*)a)->pref - ((Mx*)b)->pref;
+}
+
+/* break up an address to its component parts */
+void
+dial_string_parse(char *str, DS *ds)
+{
+        char *p, *p2;
+
+        strncpy(ds->buf, str, sizeof(ds->buf));
+        ds->buf[sizeof(ds->buf)-1] = 0;
+
+        p = strchr(ds->buf, '!');
+        if(p == 0) {
+                ds->netdir = 0;
+                ds->proto = "net";
+                ds->host = ds->buf;
+        } else {
+                if(*ds->buf != '/'){
+                        ds->netdir = 0;
+                        ds->proto = ds->buf;
+                } else {
+                        for(p2 = p; *p2 != '/'; p2--)
+                                ;
+                        *p2++ = 0;
+                        ds->netdir = ds->buf;
+                        ds->proto = p2;
+                }
+                *p = 0;
+                ds->host = p + 1;
+        }
+        ds->service = strchr(ds->host, '!');
+        if(ds->service)
+                *ds->service++ = 0;
+        if(*ds->host == '$')
+                expand_meta(ds);
+}
+
+#if 0 /* jpc */
+static void
+expand_meta(DS *ds)
+{
+        char buf[128], cs[128], *net, *p;
+        int fd, n;
+
+        net = ds->netdir;
+        if(!net)
+                net = "/net";
+
+        if(debug)
+                fprint(2, "expanding %s!%s\n", net, ds->host);
+        snprint(cs, sizeof(cs), "%s/cs", net);
+        if((fd = open(cs, ORDWR)) == -1){
+                if(debug)
+                        fprint(2, "open %s: %r\n", cs);
+                syslog(0, "smtp", "cannot open %s: %r", cs);
+                return;
+        }
+
+        snprint(buf, sizeof(buf), "!ipinfo %s", ds->host+1);        // +1 to skip $
+        if(write(fd, buf, strlen(buf)) <= 0){
+                if(debug)
+                        fprint(2, "write %s: %r\n", cs);
+                syslog(0, "smtp", "%s to %s - write failed: %r", buf, cs);
+                close(fd);
+                return;
+        }
+
+        seek(fd, 0, 0);
+        if((n = read(fd, ds->expand, sizeof(ds->expand)-1)) < 0){
+                if(debug)
+                        fprint(2, "read %s: %r\n", cs);
+                syslog(0, "smtp", "%s - read failed: %r", cs);
+                close(fd);
+                return;
+        }
+        close(fd);
+
+        ds->expand[n] = 0;
+        if((p = strchr(ds->expand, '=')) == nil){
+                if(debug)
+                        fprint(2, "response %s: %s\n", cs, ds->expand);
+                syslog(0, "smtp", "%q from %s - bad response: %r", ds->expand, cs);
+                return;
+        }
+        ds->host = p+1;
+
+        /* take only first one returned (quasi-bug) */
+        if((p = strchr(ds->host, ' ')) != nil)
+                *p = 0;
+}
+#endif /* jpc */
+
+static void
+expand_meta(DS *ds)
+{
+        Ndb *db;
+        Ndbs s;
+        char *sys, *smtpserver;
+
+        sys = sysname();
+        db = ndbopen(unsharp("#9/ndb/local"));
+        fprint(2,"%s",ds->host);
+        smtpserver = ndbgetvalue(db, &s, "sys", sys, "smtp", nil);
+        snprint(ds->host,128,"%s",smtpserver);
+        fprint(2," exanded to %s\n",ds->host);
+
+}
diff --git a/src/cmd/upas/smtp/rfc822.tab.c b/src/cmd/upas/smtp/rfc822.tab.c
t@@ -0,0 +1,1260 @@
+
+#line        2        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+#include "common.h"
+#include "smtp.h"
+#include 
+
+char        *yylp;                /* next character to be lex'd */
+int        yydone;                /* tell yylex to give up */
+char        *yybuffer;        /* first parsed character */
+char        *yyend;                /* end of buffer to be parsed */
+Node        *root;
+Field        *firstfield;
+Field        *lastfield;
+Node        *usender;
+Node        *usys;
+Node        *udate;
+char        *startfield, *endfield;
+int        originator;
+int        destination;
+int        date;
+int        received;
+int        messageid;
+extern        int        yyerrflag;
+#ifndef        YYMAXDEPTH
+#define        YYMAXDEPTH        150
+#endif
+#ifndef        YYSTYPE
+#define        YYSTYPE        int
+#endif
+YYSTYPE        yylval;
+YYSTYPE        yyval;
+#define        WORD        57346
+#define        DATE        57347
+#define        RESENT_DATE        57348
+#define        RETURN_PATH        57349
+#define        FROM        57350
+#define        SENDER        57351
+#define        REPLY_TO        57352
+#define        RESENT_FROM        57353
+#define        RESENT_SENDER        57354
+#define        RESENT_REPLY_TO        57355
+#define        SUBJECT        57356
+#define        TO        57357
+#define        CC        57358
+#define        BCC        57359
+#define        RESENT_TO        57360
+#define        RESENT_CC        57361
+#define        RESENT_BCC        57362
+#define        REMOTE        57363
+#define        PRECEDENCE        57364
+#define        MIMEVERSION        57365
+#define        CONTENTTYPE        57366
+#define        MESSAGEID        57367
+#define        RECEIVED        57368
+#define        MAILER        57369
+#define        BADTOKEN        57370
+#define YYEOFCODE 1
+#define YYERRCODE 2
+
+#line        246        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+
+
+/*
+ *  Initialize the parsing.  Done once for each header field.
+ */
+void
+yyinit(char *p, int len)
+{
+        yybuffer = p;
+        yylp = p;
+        yyend = p + len;
+        firstfield = lastfield = 0;
+        received = 0;
+}
+
+/*
+ *  keywords identifying header fields we care about
+ */
+typedef struct Keyword        Keyword;
+struct Keyword {
+        char        *rep;
+        int        val;
+};
+
+/* field names that we need to recognize */
+Keyword key[] = {
+        { "date", DATE },
+        { "resent-date", RESENT_DATE },
+        { "return_path", RETURN_PATH },
+        { "from", FROM },
+        { "sender", SENDER },
+        { "reply-to", REPLY_TO },
+        { "resent-from", RESENT_FROM },
+        { "resent-sender", RESENT_SENDER },
+        { "resent-reply-to", RESENT_REPLY_TO },
+        { "to", TO },
+        { "cc", CC },
+        { "bcc", BCC },
+        { "resent-to", RESENT_TO },
+        { "resent-cc", RESENT_CC },
+        { "resent-bcc", RESENT_BCC },
+        { "remote", REMOTE },
+        { "subject", SUBJECT },
+        { "precedence", PRECEDENCE },
+        { "mime-version", MIMEVERSION },
+        { "content-type", CONTENTTYPE },
+        { "message-id", MESSAGEID },
+        { "received", RECEIVED },
+        { "mailer", MAILER },
+        { "who-the-hell-cares", WORD }
+};
+
+/*
+ *  Lexical analysis for an rfc822 header field.  Continuation lines
+ *  are handled in yywhite() when skipping over white space.
+ *
+ */
+int
+yylex(void)
+{
+        String *t;
+        int quoting;
+        int escaping;
+        char *start;
+        Keyword *kp;
+        int c, d;
+
+/*        print("lexing\n"); /**/
+        if(yylp >= yyend)
+                return 0;
+        if(yydone)
+                return 0;
+
+        quoting = escaping = 0;
+        start = yylp;
+        yylval = malloc(sizeof(Node));
+        yylval->white = yylval->s = 0;
+        yylval->next = 0;
+        yylval->addr = 0;
+        yylval->start = yylp;
+        for(t = 0; yylp < yyend; yylp++){
+                c = *yylp & 0xff;
+
+                /* dump nulls, they can't be in header */
+                if(c == 0)
+                        continue;
+
+                if(escaping) {
+                        escaping = 0;
+                } else if(quoting) {
+                        switch(c){
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '\n':
+                                d = (*(yylp+1))&0xff;
+                                if(d != ' ' && d != '\t'){
+                                        quoting = 0;
+                                        yylp--;
+                                        continue;
+                                }
+                                break;
+                        case '"':
+                                quoting = 0;
+                                break;
+                        }
+                } else {
+                        switch(c){
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '(':
+                        case ' ':
+                        case '\t':
+                        case '\r':
+                                goto out;
+                        case '\n':
+                                if(yylp == start){
+                                        yylp++;
+/*                                        print("lex(c %c)\n", c); /**/
+                                        yylval->end = yylp;
+                                        return yylval->c = c;
+                                }
+                                goto out;
+                        case '@':
+                        case '>':
+                        case '<':
+                        case ':':
+                        case ',':
+                        case ';':
+                                if(yylp == start){
+                                        yylp++;
+                                        yylval->white = yywhite();
+/*                                        print("lex(c %c)\n", c); /**/
+                                        yylval->end = yylp;
+                                        return yylval->c = c;
+                                }
+                                goto out;
+                        case '"':
+                                quoting = 1;
+                                break;
+                        default:
+                                break;
+                        }
+                }
+                if(t == 0)
+                        t = s_new();
+                s_putc(t, c);
+        }
+out:
+        yylval->white = yywhite();
+        if(t) {
+                s_terminate(t);
+        } else                                /* message begins with white-space! */
+                return yylval->c = '\n';
+        yylval->s = t;
+        for(kp = key; kp->val != WORD; kp++)
+                if(cistrcmp(s_to_c(t), kp->rep)==0)
+                        break;
+/*        print("lex(%d) %s\n", kp->val-WORD, s_to_c(t)); /**/
+        yylval->end = yylp;
+        return yylval->c = kp->val;
+}
+
+void
+yyerror(char *x)
+{
+        USED(x);
+
+        /*fprint(2, "parse err: %s\n", x);/**/
+}
+
+/*
+ *  parse white space and comments
+ */
+String *
+yywhite(void)
+{
+        String *w;
+        int clevel;
+        int c;
+        int escaping;
+
+        escaping = clevel = 0;
+        for(w = 0; yylp < yyend; yylp++){
+                c = *yylp & 0xff;
+
+                /* dump nulls, they can't be in header */
+                if(c == 0)
+                        continue;
+
+                if(escaping){
+                        escaping = 0;
+                } else if(clevel) {
+                        switch(c){
+                        case '\n':
+                                /*
+                                 *  look for multiline fields
+                                 */
+                                if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+                                        break;
+                                else
+                                        goto out;
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '(':
+                                clevel++;
+                                break;
+                        case ')':
+                                clevel--;
+                                break;
+                        }
+                } else {
+                        switch(c){
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '(':
+                                clevel++;
+                                break;
+                        case ' ':
+                        case '\t':
+                        case '\r':
+                                break;
+                        case '\n':
+                                /*
+                                 *  look for multiline fields
+                                 */
+                                if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+                                        break;
+                                else
+                                        goto out;
+                        default:
+                                goto out;
+                        }
+                }
+                if(w == 0)
+                        w = s_new();
+                s_putc(w, c);
+        }
+out:
+        if(w)
+                s_terminate(w);
+        return w;
+}
+
+/*
+ *  link two parsed entries together
+ */
+Node*
+link2(Node *p1, Node *p2)
+{
+        Node *p;
+
+        for(p = p1; p->next; p = p->next)
+                ;
+        p->next = p2;
+        return p1;
+}
+
+/*
+ *  link three parsed entries together
+ */
+Node*
+link3(Node *p1, Node *p2, Node *p3)
+{
+        Node *p;
+
+        for(p = p2; p->next; p = p->next)
+                ;
+        p->next = p3;
+
+        for(p = p1; p->next; p = p->next)
+                ;
+        p->next = p2;
+
+        return p1;
+}
+
+/*
+ *  make a:b, move all white space after both
+ */
+Node*
+colon(Node *p1, Node *p2)
+{
+        if(p1->white){
+                if(p2->white)
+                        s_append(p1->white, s_to_c(p2->white));
+        } else {
+                p1->white = p2->white;
+                p2->white = 0;
+        }
+
+        s_append(p1->s, ":");
+        if(p2->s)
+                s_append(p1->s, s_to_c(p2->s));
+
+        if(p1->end < p2->end)
+                p1->end = p2->end;
+        freenode(p2);
+        return p1;
+}
+
+/*
+ *  concatenate two fields, move all white space after both
+ */
+Node*
+concat(Node *p1, Node *p2)
+{
+        char buf[2];
+
+        if(p1->white){
+                if(p2->white)
+                        s_append(p1->white, s_to_c(p2->white));
+        } else {
+                p1->white = p2->white;
+                p2->white = 0;
+        }
+
+        if(p1->s == nil){
+                buf[0] = p1->c;
+                buf[1] = 0;
+                p1->s = s_new();
+                s_append(p1->s, buf);
+        }
+
+        if(p2->s)
+                s_append(p1->s, s_to_c(p2->s));
+        else {
+                buf[0] = p2->c;
+                buf[1] = 0;
+                s_append(p1->s, buf);
+        }
+
+        if(p1->end < p2->end)
+                p1->end = p2->end;
+        freenode(p2);
+        return p1;
+}
+
+/*
+ *  look for disallowed chars in the field name
+ */
+int
+badfieldname(Node *p)
+{
+        for(; p; p = p->next){
+                /* field name can't contain white space */
+                if(p->white && p->next)
+                        return 1;
+        }
+        return 0;
+}
+
+/*
+ *  mark as an address
+ */
+Node *
+address(Node *p)
+{
+        p->addr = 1;
+        return p;
+}
+
+/*
+ *  case independent string compare
+ */
+int
+cistrcmp(char *s1, char *s2)
+{
+        int c1, c2;
+
+        for(; *s1; s1++, s2++){
+                c1 = isupper(*s1) ? tolower(*s1) : *s1;
+                c2 = isupper(*s2) ? tolower(*s2) : *s2;
+                if (c1 != c2)
+                        return -1;
+        }
+        return *s2;
+}
+
+/*
+ *  free a node
+ */
+void
+freenode(Node *p)
+{
+        Node *tp;
+
+        while(p){
+                tp = p->next;
+                if(p->s)
+                        s_free(p->s);
+                if(p->white)
+                        s_free(p->white);
+                free(p);
+                p = tp;
+        }
+}
+
+
+/*
+ *  an anonymous user
+ */
+Node*
+nobody(Node *p)
+{
+        if(p->s)
+                s_free(p->s);
+        p->s = s_copy("pOsTmAsTeR");
+        p->addr = 1;
+        return p;
+}
+
+/*
+ *  add anything that was dropped because of a parse error
+ */
+void
+missing(Node *p)
+{
+        Node *np;
+        char *start, *end;
+        Field *f;
+        String *s;
+
+        start = yybuffer;
+        if(lastfield != nil){
+                for(np = lastfield->node; np; np = np->next)
+                        start = np->end+1;
+        }
+
+        end = p->start-1;
+
+        if(end <= start)
+                return;
+
+        if(strncmp(start, "From ", 5) == 0)
+                return;
+
+        np = malloc(sizeof(Node));
+        np->start = start;
+        np->end = end;
+        np->white = nil;
+        s = s_copy("BadHeader: ");
+        np->s = s_nappend(s, start, end-start);
+        np->next = nil;
+
+        f = malloc(sizeof(Field));
+        f->next = 0;
+        f->node = np;
+        f->source = 0;
+        if(firstfield)
+                lastfield->next = f;
+        else
+                firstfield = f;
+        lastfield = f;
+}
+
+/*
+ *  create a new field
+ */
+void
+newfield(Node *p, int source)
+{
+        Field *f;
+
+        missing(p);
+
+        f = malloc(sizeof(Field));
+        f->next = 0;
+        f->node = p;
+        f->source = source;
+        if(firstfield)
+                lastfield->next = f;
+        else
+                firstfield = f;
+        lastfield = f;
+        endfield = startfield;
+        startfield = yylp;
+}
+
+/*
+ *  fee a list of fields
+ */
+void
+freefield(Field *f)
+{
+        Field *tf;
+
+        while(f){
+                tf = f->next;
+                freenode(f->node);
+                free(f);
+                f = tf;
+        }
+}
+
+/*
+ *  add some white space to a node
+ */
+Node*
+whiten(Node *p)
+{
+        Node *tp;
+
+        for(tp = p; tp->next; tp = tp->next)
+                ;
+        if(tp->white == 0)
+                tp->white = s_copy(" ");
+        return p;
+}
+
+void
+yycleanup(void)
+{
+        Field *f, *fnext;
+        Node *np, *next;
+
+        for(f = firstfield; f; f = fnext){
+                for(np = f->node; np; np = next){
+                        if(np->s)
+                                s_free(np->s);
+                        if(np->white)
+                                s_free(np->white);
+                        next = np->next;
+                        free(np);
+                }
+                fnext = f->next;
+                free(f);
+        }
+        firstfield = lastfield = 0;
+}
+static        const        short        yyexca[] =
+{-1, 1,
+        1, -1,
+        -2, 0,
+-1, 47,
+        1, 4,
+        -2, 0,
+-1, 112,
+        29, 72,
+        31, 72,
+        32, 72,
+        35, 72,
+        -2, 74,
+};
+#define        YYNPROD        122
+#define        YYPRIVATE 57344
+#define        YYLAST        608
+static        const        short        yyact[] =
+{
+ 112, 133, 136,  53, 121, 111, 134,  55, 109, 118,
+ 119, 116, 162, 171,  35,  48, 166,  54,   5, 166,
+ 179, 114, 115, 155,  49, 101, 100,  99,  95,  94,
+  93,  92,  98,  91, 132,  90, 123,  89, 122,  88,
+  87,  86,  85,  84,  83,  82,  97,  81,  80, 106,
+  47,  46, 110, 117, 153, 168, 108,   2,  56,  57,
+  58,  59,  60,  61,  62,  63,  64,  65,  73,  66,
+  67,  68,  69,  70,  71,  72,  74,  75,  76,  77,
+  78,  79, 124, 124,  49,  55, 177, 131, 110,  52,
+ 110, 110, 138, 137, 140, 141, 124, 124,  51, 120,
+ 124, 124, 124,  50, 102, 104, 135, 154,  31,  32,
+ 107, 157, 105,  14,  55,  55, 156,  13, 161, 117,
+ 117, 139, 158, 124, 142, 143, 144, 145, 146, 147,
+ 163, 164, 160,  12, 148, 149,  11, 157, 150, 151,
+ 152,  10, 156,   9,   8,   7,   3,   1,   0, 124,
+ 124, 124, 124, 124,   0, 169,   0,   0, 110, 165,
+   0,   0, 170, 117,   0,   0,   0,   0, 173, 176,
+ 178,   0,   0,   0, 172,   0,   0,   0, 180,   0,
+   0, 182, 183,   0,   0, 165, 165, 165, 165, 165,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0, 174,  56,  57,  58,  59,  60,  61,  62,
+  63,  64,  65,  73,  66,  67,  68,  69,  70,  71,
+  72,  74,  75,  76,  77,  78,  79,   0,   0, 128,
+ 130, 129, 125, 126, 127,  15,   0,  36,  16,  17,
+  19, 103,  20,  18,  23,  22,  21,  30,  24,  26,
+  28,  25,  27,  29,   0,  34,  37,  38,  39,  33,
+  40,   0,   4,   0,  45,  44,  41,  42,  43,  56,
+  57,  58,  59,  60,  61,  62,  63,  64,  65,  73,
+  66,  67,  68,  69,  70,  71,  72,  74,  75,  76,
+  77,  78,  79,   0,   0,  96,  45,  44,  41,  42,
+  43,  15,   0,  36,  16,  17,  19,   6,  20,  18,
+  23,  22,  21,  30,  24,  26,  28,  25,  27,  29,
+   0,  34,  37,  38,  39,  33,  40,   0,   4,   0,
+  45,  44,  41,  42,  43,  15,   0,  36,  16,  17,
+  19, 103,  20,  18,  23,  22,  21,  30,  24,  26,
+  28,  25,  27,  29,   0,  34,  37,  38,  39,  33,
+  40,   0,   0,   0,  45,  44,  41,  42,  43,  56,
+  57,  58,  59,  60,  61,  62,  63,  64,  65,  73,
+  66,  67,  68,  69,  70,  71,  72,  74,  75,  76,
+  77,  78,  79,   0,   0,   0,   0, 175, 113,   0,
+  52,  56,  57,  58,  59,  60,  61,  62,  63,  64,
+  65,  73,  66,  67,  68,  69,  70,  71,  72,  74,
+  75,  76,  77,  78,  79,   0,   0,   0,   0,   0,
+ 113,   0,  52,  56,  57,  58,  59,  60,  61,  62,
+  63,  64,  65,  73,  66,  67,  68,  69,  70,  71,
+  72,  74,  75,  76,  77,  78,  79,   0,   0,   0,
+   0,   0,   0, 159,  52,  56,  57,  58,  59,  60,
+  61,  62,  63,  64,  65,  73,  66,  67,  68,  69,
+  70,  71,  72,  74,  75,  76,  77,  78,  79,   0,
+   0,   0,   0,   0,   0,   0,  52,  56,  57,  58,
+  59,  60,  61,  62,  63,  64,  65,  73,  66,  67,
+  68,  69,  70,  71,  72,  74,  75,  76,  77,  78,
+  79,   0,   0, 167,   0,   0, 113,  56,  57,  58,
+  59,  60,  61,  62,  63,  64,  65,  73,  66,  67,
+  68,  69,  70,  71,  72,  74,  75,  76,  77,  78,
+  79,   0,   0,   0,   0,   0, 113,  56,  57,  58,
+  59,  60,  61,  62,  63,  64,  65,  73,  66,  67,
+  68,  69,  70,  71,  72,  74,  75,  76,  77,  78,
+  79,   0,   0, 181,  56,  57,  58,  59,  60,  61,
+  62,  63,  64,  65,  73,  66,  67,  68,  69,  70,
+  71,  72,  74,  75,  76,  77,  78,  79
+};
+static        const        short        yypact[] =
+{
+ 299,-1000,-1000,  22,-1000,  21,  54,-1000,-1000,-1000,
+-1000,-1000,-1000,-1000,-1000,  19,  17,  15,  14,  13,
+  12,  11,  10,   9,   7,   5,   3,   1,   0,  -1,
+  -2, 265,  -3,  -4,  -5,-1000,-1000,-1000,-1000,-1000,
+-1000,-1000,-1000,-1000,-1000,-1000, 233, 233, 580, 397,
+  -9,-1000, 580, -26, -25,-1000,-1000,-1000,-1000,-1000,
+-1000,-1000,-1000,-1000,-1000,-1000,-1000,-1000,-1000,-1000,
+-1000,-1000,-1000,-1000,-1000,-1000,-1000,-1000,-1000,-1000,
+ 333, 199, 199, 397, 461, 397, 397, 397, 397, 397,
+ 397, 397, 397, 397, 397, 199, 199,-1000,-1000, 199,
+ 199, 199,-1000,  -6,-1000,  33, 580,  -8,-1000,-1000,
+ 523,-1000,-1000, 429, 580, -23,-1000,-1000, 580, 580,
+-1000,-1000, 199,-1000,-1000,-1000,-1000,-1000,-1000,-1000,
+-1000,-1000, -15,-1000,-1000,-1000, 493,-1000,-1000, -15,
+-1000,-1000, -15, -15, -15, -15, -15, -15, 199, 199,
+ 199, 199, 199,  47, 580, 397,-1000,-1000, -21,-1000,
+ -25, -26, 580,-1000,-1000,-1000, 397, 365, 580, 580,
+-1000,-1000,-1000,-1000, -12,-1000,-1000, 553,-1000,-1000,
+ 580, 580,-1000,-1000
+};
+static        const        short        yypgo[] =
+{
+   0, 147,  57, 146,  18, 145, 144, 143, 141, 136,
+ 133, 117, 113,   8, 112,   0,  34, 110,   6,   4,
+  38, 109, 108,   1, 106,   2,   5, 103,  17,  98,
+  11,   3,  36,  86,  14
+};
+static        const        short        yyr1[] =
+{
+   0,   1,   1,   2,   2,   2,   4,   4,   4,   4,
+   4,   4,   4,   4,   4,   3,   6,   6,   6,   6,
+   6,   6,   6,   5,   5,   7,   7,   7,   7,   7,
+   7,   7,   7,   7,   7,   7,   7,   8,   8,  11,
+  11,  12,  12,  10,  10,  21,  21,  21,  21,   9,
+   9,  16,  16,  23,  23,  24,  24,  17,  17,  18,
+  18,  18,  26,  26,  13,  13,  27,  27,  29,  29,
+  28,  28,  31,  30,  25,  25,  20,  20,  32,  32,
+  32,  32,  32,  32,  32,  19,  14,  33,  33,  15,
+  15,  15,  15,  15,  15,  15,  15,  15,  15,  15,
+  15,  15,  15,  15,  15,  15,  15,  15,  15,  15,
+  15,  15,  15,  22,  22,  22,  22,  34,  34,  34,
+  34,  34
+};
+static        const        short        yyr2[] =
+{
+   0,   1,   3,   1,   2,   3,   1,   1,   1,   1,
+   1,   1,   1,   1,   3,   6,   3,   3,   3,   3,
+   3,   3,   3,   3,   3,   2,   3,   2,   3,   2,
+   3,   2,   3,   2,   3,   2,   3,   3,   2,   3,
+   2,   3,   2,   3,   2,   1,   1,   1,   1,   3,
+   2,   1,   3,   1,   1,   4,   3,   1,   3,   1,
+   2,   1,   3,   2,   3,   1,   2,   4,   1,   1,
+   3,   3,   1,   1,   1,   2,   1,   2,   1,   1,
+   1,   1,   1,   1,   1,   1,   6,   1,   3,   1,
+   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
+   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
+   1,   1,   1,   1,   1,   2,   2,   1,   1,   1,
+   1,   1
+};
+static        const        short        yychk[] =
+{
+-1000,  -1,  -2,  -3,  29,  -4,   8,  -5,  -6,  -7,
+  -8,  -9, -10, -11, -12,   2,   5,   6,  10,   7,
+   9,  13,  12,  11,  15,  18,  16,  19,  17,  20,
+  14, -22, -21,  26,  22, -34,   4,  23,  24,  25,
+  27,  33,  34,  35,  32,  31,  29,  29, -13,  30,
+ -27, -29,  35, -31, -28, -15,   4,   5,   6,   7,
+   8,   9,  10,  11,  12,  13,  15,  16,  17,  18,
+  19,  20,  21,  14,  22,  23,  24,  25,  26,  27,
+  29,  30,  30,  30,  30,  30,  30,  30,  30,  30,
+  30,  30,  30,  30,  30,  30,  30, -34, -15,  30,
+  30,  30,  -2,   8,  -2, -14, -15, -17, -18, -13,
+ -25, -26, -15,  33,  30,  31, -30, -15,  35,  35,
+  -4, -19, -20, -32, -15,  33,  34,  35,  30,  32,
+  31, -19, -16, -23, -18, -24, -25, -13, -18, -16,
+ -18, -18, -16, -16, -16, -16, -16, -16, -20, -20,
+ -20, -20, -20,  21, -15,  31, -26, -15, -13,  34,
+ -28, -31,  35, -30, -30, -32,  31,  30,   8, -15,
+ -18,  34, -30, -23, -16,  32, -15, -33, -15,  32,
+ -15,  30, -15, -15
+};
+static        const        short        yydef[] =
+{
+   0,  -2,   1,   0,   3,   0,   0,   6,   7,   8,
+   9,  10,  11,  12,  13,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0, 113, 114,  45,  46,  47,
+  48, 117, 118, 119, 120, 121,   0,  -2,   0,   0,
+   0,  65,   0,  68,  69,  72,  89,  90,  91,  92,
+  93,  94,  95,  96,  97,  98,  99, 100, 101, 102,
+ 103, 104, 105, 106, 107, 108, 109, 110, 111, 112,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,  25,
+  27,  29,  31,  33,  35,  38,  50, 115, 116,  44,
+  40,  42,   2,   0,   5,   0,   0,  18,  57,  59,
+   0,  61,  -2,   0,   0,   0,  66,  73,   0,   0,
+  14,  23,  85,  76,  78,  79,  80,  81,  82,  83,
+  84,  24,  16,  51,  53,  54,   0,  17,  19,  20,
+  21,  22,  26,  28,  30,  32,  34,  36,  37,  49,
+  43,  39,  41,   0,   0,   0,  60,  75,   0,  63,
+  64,   0,   0,  70,  71,  77,   0,   0,   0,   0,
+  58,  62,  67,  52,   0,  56,  15,   0,  87,  55,
+   0,   0,  86,  88
+};
+static        const        short        yytok1[] =
+{
+   1,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+  29,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,  31,   0,   0,   0,   0,   0,
+   0,   0,   0,   0,   0,   0,   0,   0,  30,  32,
+  33,   0,  34,   0,  35
+};
+static        const        short        yytok2[] =
+{
+   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,
+  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,
+  22,  23,  24,  25,  26,  27,  28
+};
+static        const        long        yytok3[] =
+{
+   0
+};
+#define YYFLAG                 -1000
+#define YYERROR                goto yyerrlab
+#define YYACCEPT        return(0)
+#define YYABORT                return(1)
+#define        yyclearin        yychar = -1
+#define        yyerrok                yyerrflag = 0
+
+#ifdef        yydebug
+#include        "y.debug"
+#else
+#define        yydebug                0
+static        const        char*        yytoknames[1];                /* for debugging */
+static        const        char*        yystates[1];                /* for debugging */
+#endif
+
+/*        parser for yacc output        */
+#ifdef YYARG
+#define        yynerrs                yyarg->yynerrs
+#define        yyerrflag        yyarg->yyerrflag
+#define yyval                yyarg->yyval
+#define yylval                yyarg->yylval
+#else
+int        yynerrs = 0;                /* number of errors */
+int        yyerrflag = 0;                /* error recovery flag */
+#endif
+
+extern        int        fprint(int, char*, ...);
+extern        int        sprint(char*, char*, ...);
+
+static const char*
+yytokname(int yyc)
+{
+        static char x[10];
+
+        if(yyc > 0 && yyc <= sizeof(yytoknames)/sizeof(yytoknames[0]))
+        if(yytoknames[yyc-1])
+                return yytoknames[yyc-1];
+        sprint(x, "<%d>", yyc);
+        return x;
+}
+
+static const char*
+yystatname(int yys)
+{
+        static char x[10];
+
+        if(yys >= 0 && yys < sizeof(yystates)/sizeof(yystates[0]))
+        if(yystates[yys])
+                return yystates[yys];
+        sprint(x, "<%d>\n", yys);
+        return x;
+}
+
+static long
+#ifdef YYARG
+yylex1(struct Yyarg *yyarg)
+#else
+yylex1(void)
+#endif
+{
+        long yychar;
+        const long *t3p;
+        int c;
+
+#ifdef YYARG        
+        yychar = yylex(yyarg);
+#else
+        yychar = yylex();
+#endif
+        if(yychar <= 0) {
+                c = yytok1[0];
+                goto out;
+        }
+        if(yychar < sizeof(yytok1)/sizeof(yytok1[0])) {
+                c = yytok1[yychar];
+                goto out;
+        }
+        if(yychar >= YYPRIVATE)
+                if(yychar < YYPRIVATE+sizeof(yytok2)/sizeof(yytok2[0])) {
+                        c = yytok2[yychar-YYPRIVATE];
+                        goto out;
+                }
+        for(t3p=yytok3;; t3p+=2) {
+                c = t3p[0];
+                if(c == yychar) {
+                        c = t3p[1];
+                        goto out;
+                }
+                if(c == 0)
+                        break;
+        }
+        c = 0;
+
+out:
+        if(c == 0)
+                c = yytok2[1];        /* unknown char */
+        if(yydebug >= 3)
+                fprint(2, "lex %.4lux %s\n", yychar, yytokname(c));
+        return c;
+}
+
+int
+#ifdef YYARG
+yyparse(struct Yyarg *yyarg)
+#else
+yyparse(void)
+#endif
+{
+        struct
+        {
+                YYSTYPE        yyv;
+                int        yys;
+        } yys[YYMAXDEPTH], *yyp, *yypt;
+        const short *yyxi;
+        int yyj, yym, yystate, yyn, yyg;
+        long yychar;
+#ifndef YYARG
+        YYSTYPE save1, save2;
+        int save3, save4;
+
+        save1 = yylval;
+        save2 = yyval;
+        save3 = yynerrs;
+        save4 = yyerrflag;
+#endif
+
+        yystate = 0;
+        yychar = -1;
+        yynerrs = 0;
+        yyerrflag = 0;
+        yyp = &yys[-1];
+        goto yystack;
+
+ret0:
+        yyn = 0;
+        goto ret;
+
+ret1:
+        yyn = 1;
+        goto ret;
+
+ret:
+#ifndef YYARG
+        yylval = save1;
+        yyval = save2;
+        yynerrs = save3;
+        yyerrflag = save4;
+#endif
+        return yyn;
+
+yystack:
+        /* put a state and value onto the stack */
+        if(yydebug >= 4)
+                fprint(2, "char %s in %s", yytokname(yychar), yystatname(yystate));
+
+        yyp++;
+        if(yyp >= &yys[YYMAXDEPTH]) {
+                yyerror("yacc stack overflow");
+                goto ret1;
+        }
+        yyp->yys = yystate;
+        yyp->yyv = yyval;
+
+yynewstate:
+        yyn = yypact[yystate];
+        if(yyn <= YYFLAG)
+                goto yydefault; /* simple state */
+        if(yychar < 0)
+#ifdef YYARG
+                yychar = yylex1(yyarg);
+#else
+                yychar = yylex1();
+#endif
+        yyn += yychar;
+        if(yyn < 0 || yyn >= YYLAST)
+                goto yydefault;
+        yyn = yyact[yyn];
+        if(yychk[yyn] == yychar) { /* valid shift */
+                yychar = -1;
+                yyval = yylval;
+                yystate = yyn;
+                if(yyerrflag > 0)
+                        yyerrflag--;
+                goto yystack;
+        }
+
+yydefault:
+        /* default state action */
+        yyn = yydef[yystate];
+        if(yyn == -2) {
+                if(yychar < 0)
+#ifdef YYARG
+                yychar = yylex1(yyarg);
+#else
+                yychar = yylex1();
+#endif
+
+                /* look through exception table */
+                for(yyxi=yyexca;; yyxi+=2)
+                        if(yyxi[0] == -1 && yyxi[1] == yystate)
+                                break;
+                for(yyxi += 2;; yyxi += 2) {
+                        yyn = yyxi[0];
+                        if(yyn < 0 || yyn == yychar)
+                                break;
+                }
+                yyn = yyxi[1];
+                if(yyn < 0)
+                        goto ret0;
+        }
+        if(yyn == 0) {
+                /* error ... attempt to resume parsing */
+                switch(yyerrflag) {
+                case 0:   /* brand new error */
+                        yyerror("syntax error");
+                        if(yydebug >= 1) {
+                                fprint(2, "%s", yystatname(yystate));
+                                fprint(2, "saw %s\n", yytokname(yychar));
+                        }
+                        goto yyerrlab;
+                yyerrlab:
+                        yynerrs++;
+
+                case 1:
+                case 2: /* incompletely recovered error ... try again */
+                        yyerrflag = 3;
+
+                        /* find a state where "error" is a legal shift action */
+                        while(yyp >= yys) {
+                                yyn = yypact[yyp->yys] + YYERRCODE;
+                                if(yyn >= 0 && yyn < YYLAST) {
+                                        yystate = yyact[yyn];  /* simulate a shift of "error" */
+                                        if(yychk[yystate] == YYERRCODE)
+                                                goto yystack;
+                                }
+
+                                /* the current yyp has no shift onn "error", pop stack */
+                                if(yydebug >= 2)
+                                        fprint(2, "error recovery pops state %d, uncovers %d\n",
+                                                yyp->yys, (yyp-1)->yys );
+                                yyp--;
+                        }
+                        /* there is no state on the stack with an error shift ... abort */
+                        goto ret1;
+
+                case 3:  /* no shift yet; clobber input char */
+                        if(yydebug >= 2)
+                                fprint(2, "error recovery discards %s\n", yytokname(yychar));
+                        if(yychar == YYEOFCODE)
+                                goto ret1;
+                        yychar = -1;
+                        goto yynewstate;   /* try again in the same state */
+                }
+        }
+
+        /* reduction by production yyn */
+        if(yydebug >= 2)
+                fprint(2, "reduce %d in:\n\t%s", yyn, yystatname(yystate));
+
+        yypt = yyp;
+        yyp -= yyr2[yyn];
+        yyval = (yyp+1)->yyv;
+        yym = yyn;
+
+        /* consult goto table to find next state */
+        yyn = yyr1[yyn];
+        yyg = yypgo[yyn];
+        yyj = yyg + yyp->yys + 1;
+
+        if(yyj >= YYLAST || yychk[yystate=yyact[yyj]] != -yyn)
+                yystate = yyact[yyg];
+        switch(yym) {
+                
+case 3:
+#line        56        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yydone = 1; } break;
+case 6:
+#line        61        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ date = 1; } break;
+case 7:
+#line        63        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ originator = 1; } break;
+case 8:
+#line        65        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ destination = 1; } break;
+case 15:
+#line        74        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ freenode(yypt[-5].yyv); freenode(yypt[-2].yyv); freenode(yypt[-1].yyv);
+                          usender = yypt[-4].yyv; udate = yypt[-3].yyv; usys = yypt[-0].yyv;
+                        } break;
+case 16:
+#line        79        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 1); } break;
+case 17:
+#line        81        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 1); } break;
+case 18:
+#line        83        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 1); } break;
+case 19:
+#line        85        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 1); } break;
+case 20:
+#line        87        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 1); } break;
+case 21:
+#line        89        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 1); } break;
+case 22:
+#line        91        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 1); } break;
+case 23:
+#line        94        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 24:
+#line        96        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 25:
+#line        99        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 26:
+#line        101        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 27:
+#line        103        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 28:
+#line        105        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 29:
+#line        107        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 30:
+#line        109        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 31:
+#line        111        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 32:
+#line        113        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 33:
+#line        115        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 34:
+#line        117        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 35:
+#line        119        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 36:
+#line        121        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 37:
+#line        124        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 38:
+#line        126        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 39:
+#line        129        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); received++; } break;
+case 40:
+#line        131        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); received++; } break;
+case 41:
+#line        134        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 42:
+#line        136        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 43:
+#line        139        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 44:
+#line        141        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0); } break;
+case 47:
+#line        143        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ messageid = 1; } break;
+case 49:
+#line        146        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ /* hack to allow same lex for field names and the rest */
+                         if(badfieldname(yypt[-2].yyv)){
+                                freenode(yypt[-2].yyv);
+                                freenode(yypt[-1].yyv);
+                                freenode(yypt[-0].yyv);
+                                return 1;
+                         }
+                         newfield(link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv), 0);
+                        } break;
+case 50:
+#line        156        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ /* hack to allow same lex for field names and the rest */
+                         if(badfieldname(yypt[-1].yyv)){
+                                freenode(yypt[-1].yyv);
+                                freenode(yypt[-0].yyv);
+                                return 1;
+                         }
+                         newfield(link2(yypt[-1].yyv, yypt[-0].yyv), 0);
+                        } break;
+case 52:
+#line        167        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv); } break;
+case 55:
+#line        173        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link2(yypt[-3].yyv, link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv)); } break;
+case 56:
+#line        175        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv); } break;
+case 58:
+#line        179        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv); } break;
+case 60:
+#line        183        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link2(yypt[-1].yyv, yypt[-0].yyv); } break;
+case 62:
+#line        187        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv); } break;
+case 63:
+#line        189        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = nobody(yypt[-0].yyv); freenode(yypt[-1].yyv); } break;
+case 64:
+#line        192        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = address(concat(yypt[-2].yyv, concat(yypt[-1].yyv, yypt[-0].yyv))); } break;
+case 66:
+#line        196        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = concat(yypt[-1].yyv, yypt[-0].yyv); } break;
+case 67:
+#line        198        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = concat(yypt[-3].yyv, concat(yypt[-2].yyv, concat(yypt[-1].yyv, yypt[-0].yyv))); } break;
+case 68:
+#line        201        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = address(yypt[-0].yyv); } break;
+case 70:
+#line        205        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = address(concat(yypt[-2].yyv, concat(yypt[-1].yyv, yypt[-0].yyv)));} break;
+case 71:
+#line        207        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = address(concat(yypt[-2].yyv, concat(yypt[-1].yyv, yypt[-0].yyv)));} break;
+case 75:
+#line        215        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link2(yypt[-1].yyv, yypt[-0].yyv); } break;
+case 77:
+#line        219        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link2(yypt[-1].yyv, yypt[-0].yyv); } break;
+case 86:
+#line        226        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link3(yypt[-5].yyv, yypt[-3].yyv, link3(yypt[-4].yyv, yypt[-0].yyv, link2(yypt[-2].yyv, yypt[-1].yyv))); } break;
+case 88:
+#line        230        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link3(yypt[-2].yyv, yypt[-1].yyv, yypt[-0].yyv); } break;
+case 115:
+#line        240        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link2(yypt[-1].yyv, yypt[-0].yyv); } break;
+case 116:
+#line        242        "/usr/local/plan9/src/cmd/upas/smtp/rfc822.y"
+{ yyval = link2(yypt[-1].yyv, yypt[-0].yyv); } break;
+        }
+        goto yystack;  /* stack new state and value */
+}
diff --git a/src/cmd/upas/smtp/rfc822.tab.h b/src/cmd/upas/smtp/rfc822.tab.h
t@@ -0,0 +1,98 @@
+/* A Bison parser, made by GNU Bison 2.0.  */
+
+/* Skeleton parser for Yacc-like parsing with Bison,
+   Copyright (C) 1984, 1989, 1990, 2000, 2001, 2002, 2003, 2004 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2, or (at your option)
+   any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place - Suite 330,
+   Boston, MA 02111-1307, USA.  */
+
+/* As a special exception, when this file is copied by Bison into a
+   Bison output file, you may use that output file without restriction.
+   This special exception was added by the Free Software Foundation
+   in version 1.24 of Bison.  */
+
+/* Tokens.  */
+#ifndef YYTOKENTYPE
+# define YYTOKENTYPE
+   /* Put the tokens into the symbol table, so that GDB and other debuggers
+      know about them.  */
+   enum yytokentype {
+     WORD = 258,
+     DATE = 259,
+     RESENT_DATE = 260,
+     RETURN_PATH = 261,
+     FROM = 262,
+     SENDER = 263,
+     REPLY_TO = 264,
+     RESENT_FROM = 265,
+     RESENT_SENDER = 266,
+     RESENT_REPLY_TO = 267,
+     SUBJECT = 268,
+     TO = 269,
+     CC = 270,
+     BCC = 271,
+     RESENT_TO = 272,
+     RESENT_CC = 273,
+     RESENT_BCC = 274,
+     REMOTE = 275,
+     PRECEDENCE = 276,
+     MIMEVERSION = 277,
+     CONTENTTYPE = 278,
+     MESSAGEID = 279,
+     RECEIVED = 280,
+     MAILER = 281,
+     BADTOKEN = 282
+   };
+#endif
+#define WORD 258
+#define DATE 259
+#define RESENT_DATE 260
+#define RETURN_PATH 261
+#define FROM 262
+#define SENDER 263
+#define REPLY_TO 264
+#define RESENT_FROM 265
+#define RESENT_SENDER 266
+#define RESENT_REPLY_TO 267
+#define SUBJECT 268
+#define TO 269
+#define CC 270
+#define BCC 271
+#define RESENT_TO 272
+#define RESENT_CC 273
+#define RESENT_BCC 274
+#define REMOTE 275
+#define PRECEDENCE 276
+#define MIMEVERSION 277
+#define CONTENTTYPE 278
+#define MESSAGEID 279
+#define RECEIVED 280
+#define MAILER 281
+#define BADTOKEN 282
+
+
+
+
+#if ! defined (YYSTYPE) && ! defined (YYSTYPE_IS_DECLARED)
+typedef int YYSTYPE;
+# define yystype YYSTYPE /* obsolescent; will be withdrawn */
+# define YYSTYPE_IS_DECLARED 1
+# define YYSTYPE_IS_TRIVIAL 1
+#endif
+
+extern YYSTYPE yylval;
+
+
+
diff --git a/src/cmd/upas/smtp/rfc822.y b/src/cmd/upas/smtp/rfc822.y
t@@ -0,0 +1,778 @@
+%{
+#include "common.h"
+#include "smtp.h"
+#include 
+
+char        *yylp;                /* next character to be lex'd */
+int        yydone;                /* tell yylex to give up */
+char        *yybuffer;        /* first parsed character */
+char        *yyend;                /* end of buffer to be parsed */
+Node        *root;
+Field        *firstfield;
+Field        *lastfield;
+Node        *usender;
+Node        *usys;
+Node        *udate;
+char        *startfield, *endfield;
+int        originator;
+int        destination;
+int        date;
+int        received;
+int        messageid;
+%}
+
+%term WORD
+%term DATE
+%term RESENT_DATE
+%term RETURN_PATH
+%term FROM
+%term SENDER
+%term REPLY_TO
+%term RESENT_FROM
+%term RESENT_SENDER
+%term RESENT_REPLY_TO
+%term SUBJECT
+%term TO
+%term CC
+%term BCC
+%term RESENT_TO
+%term RESENT_CC
+%term RESENT_BCC
+%term REMOTE
+%term PRECEDENCE
+%term MIMEVERSION
+%term CONTENTTYPE
+%term MESSAGEID
+%term RECEIVED
+%term MAILER
+%term BADTOKEN
+%start msg
+%%
+
+msg                : fields
+                | unixfrom '\n' fields
+                ;
+fields                : '\n'
+                        { yydone = 1; }
+                | field '\n'
+                | field '\n' fields
+                ;
+field                : dates
+                        { date = 1; }
+                | originator
+                        { originator = 1; }
+                | destination
+                        { destination = 1; }
+                | subject
+                | optional
+                | ignored
+                | received
+                | precedence
+                | error '\n' field
+                ;
+unixfrom        : FROM route_addr unix_date_time REMOTE FROM word
+                        { freenode($1); freenode($4); freenode($5);
+                          usender = $2; udate = $3; usys = $6;
+                        }
+                ;
+originator        : REPLY_TO ':' address_list
+                        { newfield(link3($1, $2, $3), 1); }
+                | RETURN_PATH ':' route_addr
+                        { newfield(link3($1, $2, $3), 1); }
+                | FROM ':' mailbox_list
+                        { newfield(link3($1, $2, $3), 1); }
+                | SENDER ':' mailbox
+                        { newfield(link3($1, $2, $3), 1); }
+                | RESENT_REPLY_TO ':' address_list
+                        { newfield(link3($1, $2, $3), 1); }
+                | RESENT_SENDER ':' mailbox
+                        { newfield(link3($1, $2, $3), 1); }
+                | RESENT_FROM ':' mailbox
+                        { newfield(link3($1, $2, $3), 1); }
+                ;
+dates                 : DATE ':' date_time
+                        { newfield(link3($1, $2, $3), 0); }
+                | RESENT_DATE ':' date_time
+                        { newfield(link3($1, $2, $3), 0); }
+                ;
+destination        : TO ':'
+                        { newfield(link2($1, $2), 0); }
+                | TO ':' address_list
+                        { newfield(link3($1, $2, $3), 0); }
+                | RESENT_TO ':'
+                        { newfield(link2($1, $2), 0); }
+                | RESENT_TO ':' address_list
+                        { newfield(link3($1, $2, $3), 0); }
+                | CC ':'
+                        { newfield(link2($1, $2), 0); }
+                | CC ':' address_list
+                        { newfield(link3($1, $2, $3), 0); }
+                | RESENT_CC ':'
+                        { newfield(link2($1, $2), 0); }
+                | RESENT_CC ':' address_list
+                        { newfield(link3($1, $2, $3), 0); }
+                | BCC ':'
+                        { newfield(link2($1, $2), 0); }
+                | BCC ':' address_list
+                        { newfield(link3($1, $2, $3), 0); }
+                | RESENT_BCC ':' 
+                        { newfield(link2($1, $2), 0); }
+                | RESENT_BCC ':' address_list
+                        { newfield(link3($1, $2, $3), 0); }
+                ;
+subject                : SUBJECT ':' things
+                        { newfield(link3($1, $2, $3), 0); }
+                | SUBJECT ':'
+                        { newfield(link2($1, $2), 0); }
+                ;
+received        : RECEIVED ':' things
+                        { newfield(link3($1, $2, $3), 0); received++; }
+                | RECEIVED ':'
+                        { newfield(link2($1, $2), 0); received++; }
+                ;
+precedence        : PRECEDENCE ':' things
+                        { newfield(link3($1, $2, $3), 0); }
+                | PRECEDENCE ':'
+                        { newfield(link2($1, $2), 0); }
+                ;
+ignored                : ignoredhdr ':' things
+                        { newfield(link3($1, $2, $3), 0); }
+                | ignoredhdr ':'
+                        { newfield(link2($1, $2), 0); }
+                ;
+ignoredhdr        : MIMEVERSION | CONTENTTYPE | MESSAGEID { messageid = 1; } | MAILER
+                ;
+optional        : fieldwords ':' things
+                        { /* hack to allow same lex for field names and the rest */
+                         if(badfieldname($1)){
+                                freenode($1);
+                                freenode($2);
+                                freenode($3);
+                                return 1;
+                         }
+                         newfield(link3($1, $2, $3), 0);
+                        }
+                | fieldwords ':'
+                        { /* hack to allow same lex for field names and the rest */
+                         if(badfieldname($1)){
+                                freenode($1);
+                                freenode($2);
+                                return 1;
+                         }
+                         newfield(link2($1, $2), 0);
+                        }
+                ;
+address_list        : address
+                | address_list ',' address
+                        { $$ = link3($1, $2, $3); }
+                ;
+address                : mailbox
+                | group
+                ;
+group                : phrase ':' address_list ';'
+                        { $$ = link2($1, link3($2, $3, $4)); }
+                | phrase ':' ';'
+                        { $$ = link3($1, $2, $3); }
+                ;
+mailbox_list        : mailbox
+                | mailbox_list ',' mailbox
+                        { $$ = link3($1, $2, $3); }
+                ;
+mailbox                : route_addr
+                | phrase brak_addr
+                        { $$ = link2($1, $2); }
+                | brak_addr
+                ;
+brak_addr        : '<' route_addr '>'
+                        { $$ = link3($1, $2, $3); }
+                | '<' '>'
+                        { $$ = nobody($2); freenode($1); }
+                ;
+route_addr        : route ':' at_addr
+                        { $$ = address(concat($1, concat($2, $3))); }
+                | addr_spec
+                ;
+route                : '@' domain
+                        { $$ = concat($1, $2); }
+                | route ',' '@' domain
+                        { $$ = concat($1, concat($2, concat($3, $4))); }
+                ;
+addr_spec        : local_part
+                        { $$ = address($1); }
+                | at_addr
+                ;
+at_addr                : local_part '@' domain
+                        { $$ = address(concat($1, concat($2, $3)));}
+                | at_addr '@' domain
+                        { $$ = address(concat($1, concat($2, $3)));}
+                ;
+local_part        : word
+                ;
+domain                : word
+                ;
+phrase                : word
+                | phrase word
+                        { $$ = link2($1, $2); }
+                ;
+things                : thing
+                | things thing
+                        { $$ = link2($1, $2); }
+                ;
+thing                : word | '<' | '>' | '@' | ':' | ';' | ','
+                ;
+date_time        : things
+                ;
+unix_date_time        : word word word unix_time word word
+                        { $$ = link3($1, $3, link3($2, $6, link2($4, $5))); }
+                ;
+unix_time        : word
+                | unix_time ':' word
+                        { $$ = link3($1, $2, $3); }
+                ;
+word                : WORD | DATE | RESENT_DATE | RETURN_PATH | FROM | SENDER
+                | REPLY_TO | RESENT_FROM | RESENT_SENDER | RESENT_REPLY_TO
+                | TO | CC | BCC | RESENT_TO | RESENT_CC | RESENT_BCC | REMOTE | SUBJECT
+                | PRECEDENCE | MIMEVERSION | CONTENTTYPE | MESSAGEID | RECEIVED | MAILER
+                ;
+fieldwords        : fieldword
+                | WORD
+                | fieldwords fieldword
+                        { $$ = link2($1, $2); }
+                | fieldwords word
+                        { $$ = link2($1, $2); }
+                ;
+fieldword        : '<' | '>' | '@' | ';' | ','
+                ;
+%%
+
+/*
+ *  Initialize the parsing.  Done once for each header field.
+ */
+void
+yyinit(char *p, int len)
+{
+        yybuffer = p;
+        yylp = p;
+        yyend = p + len;
+        firstfield = lastfield = 0;
+        received = 0;
+}
+
+/*
+ *  keywords identifying header fields we care about
+ */
+typedef struct Keyword        Keyword;
+struct Keyword {
+        char        *rep;
+        int        val;
+};
+
+/* field names that we need to recognize */
+Keyword key[] = {
+        { "date", DATE },
+        { "resent-date", RESENT_DATE },
+        { "return_path", RETURN_PATH },
+        { "from", FROM },
+        { "sender", SENDER },
+        { "reply-to", REPLY_TO },
+        { "resent-from", RESENT_FROM },
+        { "resent-sender", RESENT_SENDER },
+        { "resent-reply-to", RESENT_REPLY_TO },
+        { "to", TO },
+        { "cc", CC },
+        { "bcc", BCC },
+        { "resent-to", RESENT_TO },
+        { "resent-cc", RESENT_CC },
+        { "resent-bcc", RESENT_BCC },
+        { "remote", REMOTE },
+        { "subject", SUBJECT },
+        { "precedence", PRECEDENCE },
+        { "mime-version", MIMEVERSION },
+        { "content-type", CONTENTTYPE },
+        { "message-id", MESSAGEID },
+        { "received", RECEIVED },
+        { "mailer", MAILER },
+        { "who-the-hell-cares", WORD }
+};
+
+/*
+ *  Lexical analysis for an rfc822 header field.  Continuation lines
+ *  are handled in yywhite() when skipping over white space.
+ *
+ */
+int
+yylex(void)
+{
+        String *t;
+        int quoting;
+        int escaping;
+        char *start;
+        Keyword *kp;
+        int c, d;
+
+/*        print("lexing\n"); /**/
+        if(yylp >= yyend)
+                return 0;
+        if(yydone)
+                return 0;
+
+        quoting = escaping = 0;
+        start = yylp;
+        yylval = malloc(sizeof(Node));
+        yylval->white = yylval->s = 0;
+        yylval->next = 0;
+        yylval->addr = 0;
+        yylval->start = yylp;
+        for(t = 0; yylp < yyend; yylp++){
+                c = *yylp & 0xff;
+
+                /* dump nulls, they can't be in header */
+                if(c == 0)
+                        continue;
+
+                if(escaping) {
+                        escaping = 0;
+                } else if(quoting) {
+                        switch(c){
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '\n':
+                                d = (*(yylp+1))&0xff;
+                                if(d != ' ' && d != '\t'){
+                                        quoting = 0;
+                                        yylp--;
+                                        continue;
+                                }
+                                break;
+                        case '"':
+                                quoting = 0;
+                                break;
+                        }
+                } else {
+                        switch(c){
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '(':
+                        case ' ':
+                        case '\t':
+                        case '\r':
+                                goto out;
+                        case '\n':
+                                if(yylp == start){
+                                        yylp++;
+/*                                        print("lex(c %c)\n", c); /**/
+                                        yylval->end = yylp;
+                                        return yylval->c = c;
+                                }
+                                goto out;
+                        case '@':
+                        case '>':
+                        case '<':
+                        case ':':
+                        case ',':
+                        case ';':
+                                if(yylp == start){
+                                        yylp++;
+                                        yylval->white = yywhite();
+/*                                        print("lex(c %c)\n", c); /**/
+                                        yylval->end = yylp;
+                                        return yylval->c = c;
+                                }
+                                goto out;
+                        case '"':
+                                quoting = 1;
+                                break;
+                        default:
+                                break;
+                        }
+                }
+                if(t == 0)
+                        t = s_new();
+                s_putc(t, c);
+        }
+out:
+        yylval->white = yywhite();
+        if(t) {
+                s_terminate(t);
+        } else                                /* message begins with white-space! */
+                return yylval->c = '\n';
+        yylval->s = t;
+        for(kp = key; kp->val != WORD; kp++)
+                if(cistrcmp(s_to_c(t), kp->rep)==0)
+                        break;
+/*        print("lex(%d) %s\n", kp->val-WORD, s_to_c(t)); /**/
+        yylval->end = yylp;
+        return yylval->c = kp->val;
+}
+
+void
+yyerror(char *x)
+{
+        USED(x);
+
+        /*fprint(2, "parse err: %s\n", x);/**/
+}
+
+/*
+ *  parse white space and comments
+ */
+String *
+yywhite(void)
+{
+        String *w;
+        int clevel;
+        int c;
+        int escaping;
+
+        escaping = clevel = 0;
+        for(w = 0; yylp < yyend; yylp++){
+                c = *yylp & 0xff;
+
+                /* dump nulls, they can't be in header */
+                if(c == 0)
+                        continue;
+
+                if(escaping){
+                        escaping = 0;
+                } else if(clevel) {
+                        switch(c){
+                        case '\n':
+                                /*
+                                 *  look for multiline fields
+                                 */
+                                if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+                                        break;
+                                else
+                                        goto out;
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '(':
+                                clevel++;
+                                break;
+                        case ')':
+                                clevel--;
+                                break;
+                        }
+                } else {
+                        switch(c){
+                        case '\\':
+                                escaping = 1;
+                                break;
+                        case '(':
+                                clevel++;
+                                break;
+                        case ' ':
+                        case '\t':
+                        case '\r':
+                                break;
+                        case '\n':
+                                /*
+                                 *  look for multiline fields
+                                 */
+                                if(*(yylp+1)==' ' || *(yylp+1)=='\t')
+                                        break;
+                                else
+                                        goto out;
+                        default:
+                                goto out;
+                        }
+                }
+                if(w == 0)
+                        w = s_new();
+                s_putc(w, c);
+        }
+out:
+        if(w)
+                s_terminate(w);
+        return w;
+}
+
+/*
+ *  link two parsed entries together
+ */
+Node*
+link2(Node *p1, Node *p2)
+{
+        Node *p;
+
+        for(p = p1; p->next; p = p->next)
+                ;
+        p->next = p2;
+        return p1;
+}
+
+/*
+ *  link three parsed entries together
+ */
+Node*
+link3(Node *p1, Node *p2, Node *p3)
+{
+        Node *p;
+
+        for(p = p2; p->next; p = p->next)
+                ;
+        p->next = p3;
+
+        for(p = p1; p->next; p = p->next)
+                ;
+        p->next = p2;
+
+        return p1;
+}
+
+/*
+ *  make a:b, move all white space after both
+ */
+Node*
+colon(Node *p1, Node *p2)
+{
+        if(p1->white){
+                if(p2->white)
+                        s_append(p1->white, s_to_c(p2->white));
+        } else {
+                p1->white = p2->white;
+                p2->white = 0;
+        }
+
+        s_append(p1->s, ":");
+        if(p2->s)
+                s_append(p1->s, s_to_c(p2->s));
+
+        if(p1->end < p2->end)
+                p1->end = p2->end;
+        freenode(p2);
+        return p1;
+}
+
+/*
+ *  concatenate two fields, move all white space after both
+ */
+Node*
+concat(Node *p1, Node *p2)
+{
+        char buf[2];
+
+        if(p1->white){
+                if(p2->white)
+                        s_append(p1->white, s_to_c(p2->white));
+        } else {
+                p1->white = p2->white;
+                p2->white = 0;
+        }
+
+        if(p1->s == nil){
+                buf[0] = p1->c;
+                buf[1] = 0;
+                p1->s = s_new();
+                s_append(p1->s, buf);
+        }
+
+        if(p2->s)
+                s_append(p1->s, s_to_c(p2->s));
+        else {
+                buf[0] = p2->c;
+                buf[1] = 0;
+                s_append(p1->s, buf);
+        }
+
+        if(p1->end < p2->end)
+                p1->end = p2->end;
+        freenode(p2);
+        return p1;
+}
+
+/*
+ *  look for disallowed chars in the field name
+ */
+int
+badfieldname(Node *p)
+{
+        for(; p; p = p->next){
+                /* field name can't contain white space */
+                if(p->white && p->next)
+                        return 1;
+        }
+        return 0;
+}
+
+/*
+ *  mark as an address
+ */
+Node *
+address(Node *p)
+{
+        p->addr = 1;
+        return p;
+}
+
+/*
+ *  case independent string compare
+ */
+int
+cistrcmp(char *s1, char *s2)
+{
+        int c1, c2;
+
+        for(; *s1; s1++, s2++){
+                c1 = isupper(*s1) ? tolower(*s1) : *s1;
+                c2 = isupper(*s2) ? tolower(*s2) : *s2;
+                if (c1 != c2)
+                        return -1;
+        }
+        return *s2;
+}
+
+/*
+ *  free a node
+ */
+void
+freenode(Node *p)
+{
+        Node *tp;
+
+        while(p){
+                tp = p->next;
+                if(p->s)
+                        s_free(p->s);
+                if(p->white)
+                        s_free(p->white);
+                free(p);
+                p = tp;
+        }
+}
+
+
+/*
+ *  an anonymous user
+ */
+Node*
+nobody(Node *p)
+{
+        if(p->s)
+                s_free(p->s);
+        p->s = s_copy("pOsTmAsTeR");
+        p->addr = 1;
+        return p;
+}
+
+/*
+ *  add anything that was dropped because of a parse error
+ */
+void
+missing(Node *p)
+{
+        Node *np;
+        char *start, *end;
+        Field *f;
+        String *s;
+
+        start = yybuffer;
+        if(lastfield != nil){
+                for(np = lastfield->node; np; np = np->next)
+                        start = np->end+1;
+        }
+
+        end = p->start-1;
+
+        if(end <= start)
+                return;
+
+        if(strncmp(start, "From ", 5) == 0)
+                return;
+
+        np = malloc(sizeof(Node));
+        np->start = start;
+        np->end = end;
+        np->white = nil;
+        s = s_copy("BadHeader: ");
+        np->s = s_nappend(s, start, end-start);
+        np->next = nil;
+
+        f = malloc(sizeof(Field));
+        f->next = 0;
+        f->node = np;
+        f->source = 0;
+        if(firstfield)
+                lastfield->next = f;
+        else
+                firstfield = f;
+        lastfield = f;
+}
+
+/*
+ *  create a new field
+ */
+void
+newfield(Node *p, int source)
+{
+        Field *f;
+
+        missing(p);
+
+        f = malloc(sizeof(Field));
+        f->next = 0;
+        f->node = p;
+        f->source = source;
+        if(firstfield)
+                lastfield->next = f;
+        else
+                firstfield = f;
+        lastfield = f;
+        endfield = startfield;
+        startfield = yylp;
+}
+
+/*
+ *  fee a list of fields
+ */
+void
+freefield(Field *f)
+{
+        Field *tf;
+
+        while(f){
+                tf = f->next;
+                freenode(f->node);
+                free(f);
+                f = tf;
+        }
+}
+
+/*
+ *  add some white space to a node
+ */
+Node*
+whiten(Node *p)
+{
+        Node *tp;
+
+        for(tp = p; tp->next; tp = tp->next)
+                ;
+        if(tp->white == 0)
+                tp->white = s_copy(" ");
+        return p;
+}
+
+void
+yycleanup(void)
+{
+        Field *f, *fnext;
+        Node *np, *next;
+
+        for(f = firstfield; f; f = fnext){
+                for(np = f->node; np; np = next){
+                        if(np->s)
+                                s_free(np->s);
+                        if(np->white)
+                                s_free(np->white);
+                        next = np->next;
+                        free(np);
+                }
+                fnext = f->next;
+                free(f);
+        }
+        firstfield = lastfield = 0;
+}
diff --git a/src/cmd/upas/smtp/rmtdns.c b/src/cmd/upas/smtp/rmtdns.c
t@@ -0,0 +1,58 @@
+#include        "common.h"
+#include        
+
+int
+rmtdns(char *net, char *path)
+{
+
+        int fd, n, r;
+        char *domain, *cp, buf[1024];
+
+        if(net == 0 || path == 0)
+                return 0;
+
+        domain = strdup(path);
+        cp = strchr(domain, '!');
+        if(cp){
+                *cp = 0;
+                n = cp-domain;
+        } else
+                n = strlen(domain);
+
+        if(*domain == '[' && domain[n-1] == ']'){        /* accept [nnn.nnn.nnn.nnn] */
+                domain[n-1] = 0;
+                r = strcmp(ipattr(domain+1), "ip");
+                domain[n-1] = ']';
+        } else
+                r = strcmp(ipattr(domain), "ip");        /* accept nnn.nnn.nnn.nnn */
+
+        if(r == 0){
+                free(domain);
+                return 0;
+        }
+
+        snprint(buf, sizeof(buf), "%s/dns", net);
+
+        fd = open(buf, ORDWR);                        /* look up all others */
+        if(fd < 0){                                /* dns screw up - can't check */
+                free(domain);
+                return 0;
+        }
+
+        n = snprint(buf, sizeof(buf), "%s all", domain);
+        free(domain);
+        seek(fd, 0, 0);
+        n = write(fd, buf, n);
+        close(fd);
+        if(n < 0){
+                rerrstr(buf, sizeof(buf));
+                if (strcmp(buf, "dns: name does not exist") == 0)
+                        return -1;
+        }
+        return 0;
+}
+
+/*
+void main(int, char *argv[]){ print("return = %d\n", rmtdns("/net.alt/tcp/109", argv[1]));}
+
+*/
diff --git a/src/cmd/upas/smtp/smtp.c b/src/cmd/upas/smtp/smtp.c
t@@ -0,0 +1,1122 @@
+#include "common.h"
+#include "smtp.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+static        char*        connect(char*);
+static        char*        dotls(char*);
+static        char*        doauth(char*);
+char*        hello(char*, int);
+char*        mailfrom(char*);
+char*        rcptto(char*);
+char*        data(String*, Biobuf*);
+void        quit(char*);
+int        getreply(void);
+void        addhostdom(String*, char*);
+String*        bangtoat(char*);
+String*        convertheader(String*);
+int        printheader(void);
+char*        domainify(char*, char*);
+void        putcrnl(char*, int);
+char*        getcrnl(String*);
+int        printdate(Node*);
+char        *rewritezone(char *);
+int        dBprint(char*, ...);
+int        dBputc(int);
+String*        fixrouteaddr(String*, Node*, Node*);
+char* expand_addr(char* a);
+int        ping;
+int        insecure;
+
+#define Retry        "Retry, Temporary Failure"
+#define Giveup        "Permanent Failure"
+
+int        debug;                /* true if we're debugging */
+String        *reply;                /* last reply */
+String        *toline;
+int        alarmscale;
+int        last = 'n';        /* last character sent by putcrnl() */
+int        filter;
+int        trysecure;        /* Try to use TLS if the other side supports it */
+int        tryauth;        /* Try to authenticate, if supported */
+int        quitting;        /* when error occurs in quit */
+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;
+
+void
+usage(void)
+{
+        fprint(2, "usage: smtp [-adips] [-uuser] [-hhost] [.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
+threadmain(int argc, char **argv)
+{
+        char hellodomain[256];
+        char *host, *domain;
+        String *from;
+        String *fromm;
+        String *sender;
+        char *addr;
+        char *rv, *trv;
+        int i, ok, rcvrs;
+        char **errs;
+
+        alarmscale = 60*1000;        /* minutes */
+        quotefmtinstall();
+        errs = malloc(argc*sizeof(char*));
+        reply = s_new();
+        host = 0;
+        ARGBEGIN{
+        case 'a':
+                tryauth = 1;
+                trysecure = 1;
+                break;
+        case 'f':
+                filter = 1;
+                break;
+        case 'd':
+                debug = 1;
+                break;
+        case 'g':
+                gdomain = ARGF();
+                break;
+        case 'h':
+                host = ARGF();
+                break;
+        case 'i':
+                insecure = 1;
+                break;
+        case 'p':
+                alarmscale = 10*1000;        /* tens of seconds */
+                ping = 1;
+                break;
+        case 's':
+                trysecure = 1;
+                break;
+        case 'u':
+                user = ARGF();
+                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--;
+        // expand $smtp if necessary
+        addr = expand_addr(addr);
+        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);
+        }
+
+        /* 10 minutes to get through the initial handshake */
+        atnotify(timeout, 1);
+
+        alarm(10*alarmscale);
+        if((rv = connect(addr)) != 0)
+                exits(rv);
+        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[256];
+        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";
+
+/*
+ *  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;
+        Thumbprint *goodcerts;
+        char *h;
+        int fd;
+        uchar hash[SHA1dlen];
+
+        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;
+        }
+        goodcerts = initThumbprints(smtpthumbs, smtpexclthumbs);
+        if (goodcerts == nil) {
+                free(c);
+                close(fd);
+                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)) {
+                /* TODO? if not excluded, add hash to thumb list */
+                free(c);
+                close(fd);
+                h = malloc(2*sizeof hash + 1);
+                if (h != nil) {
+                        enc16(h, 2*sizeof hash + 1, hash, sizeof hash);
+                        // print("x509 sha1=%s", h);
+                        syslog(0, "smtp",
+                "remote cert. has bad thumbprint: x509 sha1=%s server=%q",
+                                h, ddomain);
+                        free(h);
+                }
+                return Giveup;                /* how to recover? TLS is started */
+        }
+        freeThumbprints(goodcerts);
+        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)
+                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(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 1){
+                addhostdom(buf, field[0]);
+                for(j=1; j");
+                        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'){
+                                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;
+                }
+        }
+        return 0;
+}
+
+/*
+ *  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);
+}
+
+char* 
+expand_addr(char* a)
+{
+        Ndb *db;
+        Ndbs s;
+        char *sys, *ret, *proto, *host;
+
+        proto = strtok(a,"!");
+        if ( strcmp(proto,"net") != 0 ) {
+                fprint(2,"unknown proto %s\n",proto);
+        }
+        host = strtok(0,"!");
+        if ( strcmp(host,"$smtp") == 0 ) {
+                sys = sysname();
+                db = ndbopen(unsharp("#9/ndb/local"));
+                host = ndbgetvalue(db, &s, "sys", sys, "smtp", nil);
+        }
+        ret = malloc(strlen(proto)+strlen(host)+2);
+        sprint(ret,"%s!%s",proto,host);
+
+        return ret;
+
+}
diff --git a/src/cmd/upas/smtp/smtp.h b/src/cmd/upas/smtp/smtp.h
t@@ -0,0 +1,61 @@
+typedef struct Node Node;
+typedef struct Field Field;
+typedef Node *Nodeptr;
+#define YYSTYPE Nodeptr
+
+struct Node {
+        Node        *next;
+        int        c;        /* token type */
+        char        addr;        /* true if this is an address */
+        String        *s;        /* string representing token */
+        String        *white;        /* white space following token */
+        char        *start;        /* first byte for this token */
+        char        *end;        /* next byte in input */
+};
+
+struct Field {
+        Field        *next;
+        Node        *node;
+        int        source;
+};
+
+typedef struct DS        DS;
+struct DS {
+        /* dist string */
+        char        buf[128];
+        char        expand[128];
+        char        *netdir;
+        char        *proto;
+        char        *host;
+        char        *service;
+};
+
+extern Field        *firstfield;
+extern Field        *lastfield;
+extern Node        *usender;
+extern Node        *usys;
+extern Node        *udate;
+extern int        originator;
+extern int        destination;
+extern int        date;
+extern int        messageid;
+
+Node*        anonymous(Node*);
+Node*        address(Node*);
+int        badfieldname(Node*);
+Node*        bang(Node*, Node*);
+Node*        colon(Node*, Node*);
+int        cistrcmp(char*, char*);
+Node*        link2(Node*, Node*);
+Node*        link3(Node*, Node*, Node*);
+void        freenode(Node*);
+void        newfield(Node*, int);
+void        freefield(Field*);
+void        yyinit(char*, int);
+int        yyparse(void);
+int        yylex(void);
+String*        yywhite(void);
+Node*        whiten(Node*);
+void        yycleanup(void);
+int        mxdial(char*, char*, char*);
+void        dial_string_parse(char*, DS*);
diff --git a/src/cmd/upas/smtp/smtpd.c b/src/cmd/upas/smtp/smtpd.c
t@@ -0,0 +1,1494 @@
+#include "common.h"
+#include "smtpd.h"
+#include "smtp.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include "../smtp/y.tab.h"
+
+#define DBGMX 1
+
+char        *me;
+char        *him="";
+char        *dom;
+process        *pp;
+String        *mailer;
+NetConnInfo *nci;
+
+int        filterstate = ACCEPT;
+int        trusted;
+int        logged;
+int        rejectcount;
+int        hardreject;
+
+Biobuf        bin;
+
+int        debug;
+int        Dflag;
+int        fflag;
+int        gflag;
+int        rflag;
+int        sflag;
+int        authenticate;
+int        authenticated;
+int        passwordinclear;
+char        *tlscert;
+
+List        senders;
+List        rcvers;
+
+char pipbuf[ERRMAX];
+char        *piperror;
+int        pipemsg(int*);
+String*        startcmd(void);
+int        rejectcheck(void);
+String*        mailerpath(char*);
+
+static int
+catchalarm(void *a, char *msg)
+{
+        int rv = 1;
+
+        USED(a);
+
+        /* log alarms but continue */
+        if(strstr(msg, "alarm")){
+                if(senders.first && rcvers.first)
+                        syslog(0, "smtpd", "note: %s->%s: %s", s_to_c(senders.first->p),
+                                s_to_c(rcvers.first->p), msg);
+                else
+                        syslog(0, "smtpd", "note: %s", msg);
+                rv = 0;
+        }
+
+        /* kill the children if there are any */
+        if(pp)
+                syskillpg(pp->pid);
+
+        return rv;
+}
+
+        /* override string error functions to do something reasonable */
+void
+s_error(char *f, char *status)
+{
+        char errbuf[Errlen];
+
+        errbuf[0] = 0;
+        rerrstr(errbuf, sizeof(errbuf));
+        if(f && *f)
+                reply("452 out of memory %s: %s\r\n", f, errbuf);
+        else
+                reply("452 out of memory %s\r\n", errbuf);
+        syslog(0, "smtpd", "++Malloc failure %s [%s]", him, nci->rsys);
+        exits(status);
+}
+
+void
+main(int argc, char **argv)
+{
+        char *p, buf[1024];
+        char *netdir;
+
+        netdir = nil;
+        quotefmtinstall();
+        ARGBEGIN{
+        case 'D':
+                Dflag++;
+                break;
+        case 'd':
+                debug++;
+                break;
+        case 'n':                                /* log peer ip address */
+                netdir = ARGF();
+                break;
+        case 'f':                                /* disallow relaying */
+                fflag = 1;
+                break;
+        case 'g':
+                gflag = 1;
+                break;
+        case 'h':                                /* default domain name */
+                dom = ARGF();
+                break;
+        case 'k':                                /* prohibited ip address */
+                p = ARGF();
+                if (p)
+                        addbadguy(p);
+                break;
+        case 'm':                                /* set mail command */
+                p = ARGF();
+                if(p)
+                        mailer = mailerpath(p);
+                break;
+        case 'r':
+                rflag = 1;                        /* verify sender's domain */
+                break;
+        case 's':                                /* save blocked messages */
+                sflag = 1;
+                break;
+        case 'a':
+                authenticate = 1;
+                break;
+        case 'p':
+                passwordinclear = 1;
+                break;
+        case 'c':
+                tlscert = ARGF();
+                break;
+        case 't':
+                fprint(2, "%s: the -t option is no longer supported, see -c\n", argv0);
+                tlscert = "/sys/lib/ssl/smtpd-cert.pem";
+                break;
+        default:
+                fprint(2, "usage: smtpd [-dfhrs] [-n net] [-c cert]\n");
+                exits("usage");
+        }ARGEND;
+
+        nci = getnetconninfo(netdir, 0);
+        if(nci == nil)
+                sysfatal("can't get remote system's address");
+
+        if(mailer == nil)
+                mailer = mailerpath("send");
+
+        if(debug){
+                close(2);
+                snprint(buf, sizeof(buf), "%s/smtpd", UPASLOG);
+                if (open(buf, OWRITE) >= 0) {
+                        seek(2, 0, 2);
+                        fprint(2, "%d smtpd %s\n", getpid(), thedate());
+                } else
+                        debug = 0;
+        }
+        getconf();
+        Binit(&bin, 0, OREAD);
+
+        chdir(UPASLOG);
+        me = sysname_read();
+        if(dom == 0 || dom[0] == 0)
+                dom = domainname_read();
+        if(dom == 0 || dom[0] == 0)
+                dom = me;
+        sayhi();
+        parseinit();
+                /* allow 45 minutes to parse the header */
+        atnotify(catchalarm, 1);
+        alarm(45*60*1000);
+        zzparse();
+        exits(0);
+}
+
+void
+listfree(List *l)
+{
+        Link *lp;
+        Link *next;
+
+        for(lp = l->first; lp; lp = next){
+                next = lp->next;
+                s_free(lp->p);
+                free(lp);
+        }
+        l->first = l->last = 0;
+}
+
+void
+listadd(List *l, String *path)
+{
+        Link *lp;
+
+        lp = (Link *)malloc(sizeof(Link));
+        lp->p = path;
+        lp->next = 0;
+
+        if(l->last)
+                l->last->next = lp;
+        else
+                l->first = lp;
+        l->last = lp;
+}
+
+#define        SIZE        4096
+int
+reply(char *fmt, ...)
+{
+        char buf[SIZE], *out;
+        va_list arg;
+        int n;
+
+        va_start(arg, fmt);
+        out = vseprint(buf, buf+SIZE, fmt, arg);
+        va_end(arg);
+        n = (long)(out-buf);
+        if(debug) {
+                seek(2, 0, 2);
+                write(2, buf, n);
+        }
+        write(1, buf, n);
+        return n;
+}
+
+void
+reset(void)
+{
+        if(rejectcheck())
+                return;
+        listfree(&rcvers);
+        listfree(&senders);
+        if(filterstate != DIALUP){
+                logged = 0;
+                filterstate = ACCEPT;
+        }
+        reply("250 ok\r\n");
+}
+
+void
+sayhi(void)
+{
+        reply("220 %s SMTP\r\n", dom);
+}
+
+void
+hello(String *himp, int extended)
+{
+        char **mynames;
+
+        him = s_to_c(himp);
+        syslog(0, "smtpd", "%s from %s as %s", extended ? "ehlo" : "helo", nci->rsys, him);
+        if(rejectcheck())
+                return;
+
+        if(strchr(him, '.') && nci && !trusted && fflag && strcmp(nci->rsys, nci->lsys) != 0){
+                /*
+                 * We don't care if he lies about who he is, but it is
+                 * not okay to pretend to be us.  Many viruses do this,
+                 * just parroting back what we say in the greeting.
+                 */
+                if(strcmp(him, dom) == 0)
+                        goto Liarliar;
+                for(mynames=sysnames_read(); mynames && *mynames; mynames++){
+                        if(cistrcmp(*mynames, him) == 0){
+                        Liarliar:
+                                syslog(0, "smtpd", "Hung up on %s; claimed to be %s",
+                                        nci->rsys, him);
+                                reply("554 Liar!\r\n");
+                                exits("client pretended to be us");
+                                return;
+                        }
+                }
+        }
+        /*
+         * it is never acceptable to claim to be "localhost",
+         * "localhost.localdomain" or "localhost.example.com"; only spammers
+         * do this.  it should be unacceptable to claim any string that doesn't
+         * look like a domain name (e.g., has at least one dot in it), but
+         * Microsoft mail software gets this wrong.
+         */
+        if (strcmp(him, "localhost") == 0 ||
+            strcmp(him, "localhost.localdomain") == 0 ||
+            strcmp(him, "localhost.example.com") == 0)
+                goto Liarliar;
+        if(strchr(him, '.') == 0 && nci != nil && strchr(nci->rsys, '.') != nil)
+                him = nci->rsys;
+
+        if(Dflag)
+                sleep(15*1000);
+        reply("250%c%s you are %s\r\n", extended ? '-' : ' ', dom, him);
+        if (extended) {
+                if(tlscert != nil)
+                        reply("250-STARTTLS\r\n");
+                if (passwordinclear)                
+                        reply("250 AUTH CRAM-MD5 PLAIN LOGIN\r\n");
+                else
+                        reply("250 AUTH CRAM-MD5\r\n");
+        }
+}
+
+void
+sender(String *path)
+{
+        String *s;
+        static char *lastsender;
+
+        if(rejectcheck())
+                return;
+        if (authenticate && !authenticated) {
+                rejectcount++;
+                reply("530 Authentication required\r\n");
+                return;
+        }
+        if(him == 0 || *him == 0){
+                rejectcount++;
+                reply("503 Start by saying HELO, please.\r\n", s_to_c(path));
+                return;
+        }
+
+        /* don't add the domain onto black holes or we will loop */
+        if(strchr(s_to_c(path), '!') == 0 && strcmp(s_to_c(path), "/dev/null") != 0){
+                s = s_new();
+                s_append(s, him);
+                s_append(s, "!");
+                s_append(s, s_to_c(path));
+                s_terminate(s);
+                s_free(path);
+                path = s;
+        }
+        if(shellchars(s_to_c(path))){
+                rejectcount++;
+                reply("503 Bad character in sender address %s.\r\n", s_to_c(path));
+                return;
+        }
+
+        /*
+         * if the last sender address resulted in a rejection because the sending
+         * domain didn't exist and this sender has the same domain, reject immediately.
+         */
+        if(lastsender){
+                if (strncmp(lastsender, s_to_c(path), strlen(lastsender)) == 0){
+                        filterstate = REFUSED;
+                        rejectcount++;
+                        reply("554 Sender domain must exist: %s\r\n", s_to_c(path));
+                        return;
+                }
+                free(lastsender);        /* different sender domain */
+                lastsender = 0;
+        }
+
+        /*
+         * see if this ip address, domain name, user name or account is blocked
+         */
+        filterstate = blocked(path);
+
+        logged = 0;
+        listadd(&senders, path);
+        reply("250 sender is %s\r\n", s_to_c(path));
+}
+
+enum { Rcpt, Domain, Ntoks };
+
+typedef struct Sender Sender;
+struct Sender {
+        Sender        *next;
+        char        *rcpt;
+        char        *domain;
+};
+static Sender *sendlist, *sendlast;
+static uchar rsysip[IPaddrlen];
+
+static int
+rdsenders(void)
+{
+        int lnlen, nf, ok = 1;
+        char *line, *senderfile;
+        char *toks[Ntoks];
+        Biobuf *sf;
+        Sender *snd;
+        static int beenhere = 0;
+
+        if (beenhere)
+                return 1;
+        beenhere = 1;
+
+        fmtinstall('I', eipfmt);
+        parseip(rsysip, nci->rsys);
+
+        /*
+         * we're sticking with a system-wide sender list because
+         * per-user lists would require fully resolving recipient
+         * addresses to determine which users they correspond to
+         * (barring syntactic conventions).
+         */
+        senderfile = smprint("%s/senders", UPASLIB);
+        sf = Bopen(senderfile, OREAD);
+        free(senderfile);
+        if (sf == nil)
+                return 1;
+        while ((line = Brdline(sf, '\n')) != nil) {
+                if (line[0] == '#' || line[0] == '\n')
+                        continue;
+                lnlen = Blinelen(sf);
+                line[lnlen-1] = '\0';                /* clobber newline */
+                nf = tokenize(line, toks, nelem(toks));
+                if (nf != nelem(toks))
+                        continue;                /* malformed line */
+
+                snd = malloc(sizeof *snd);
+                if (snd == nil)
+                        sysfatal("out of memory: %r");
+                memset(snd, 0, sizeof *snd);
+                snd->next = nil;
+
+                if (sendlast == nil)
+                        sendlist = snd;
+                else
+                        sendlast->next = snd;
+                sendlast = snd;
+                snd->rcpt = strdup(toks[Rcpt]);
+                snd->domain = strdup(toks[Domain]);
+        }
+        Bterm(sf);
+        return ok;
+}
+
+/*
+ * read (recipient, sender's DNS) pairs from /mail/lib/senders.
+ * Only allow mail to recipient from any of sender's IPs.
+ * A recipient not mentioned in the file is always permitted.
+ */
+static int
+senderok(char *rcpt)
+{
+        int mentioned = 0, matched = 0;
+        uchar dnsip[IPaddrlen];
+        Sender *snd;
+        Ndbtuple *nt, *next, *first;
+
+        rdsenders();
+        for (snd = sendlist; snd != nil; snd = snd->next) {
+                if (strcmp(rcpt, snd->rcpt) != 0)
+                        continue;
+                /*
+                 * see if this domain's ips match nci->rsys.
+                 * if not, perhaps a later entry's domain will.
+                 */
+                mentioned = 1;
+                if (parseip(dnsip, snd->domain) != -1 &&
+                    memcmp(rsysip, dnsip, IPaddrlen) == 0)
+                        return 1;
+                /*
+                 * NB: nt->line links form a circular list(!).
+                 * we need to make one complete pass over it to free it all.
+                 */
+                first = nt = dnsquery(nci->root, snd->domain, "ip");
+                if (first == nil)
+                        continue;
+                do {
+                        if (strcmp(nt->attr, "ip") == 0 &&
+                            parseip(dnsip, nt->val) != -1 &&
+                            memcmp(rsysip, dnsip, IPaddrlen) == 0)
+                                matched = 1;
+                        next = nt->line;
+                        free(nt);
+                        nt = next;
+                } while (nt != first);
+        }
+        if (matched)
+                return 1;
+        else
+                return !mentioned;
+}
+
+void
+receiver(String *path)
+{
+        char *sender, *rcpt;
+
+        if(rejectcheck())
+                return;
+        if(him == 0 || *him == 0){
+                rejectcount++;
+                reply("503 Start by saying HELO, please\r\n");
+                return;
+        }
+        if(senders.last)
+                sender = s_to_c(senders.last->p);
+        else
+                sender = "";
+
+        if(!recipok(s_to_c(path))){
+                rejectcount++;
+                syslog(0, "smtpd", "Disallowed %s (%s/%s) to blocked name %s",
+                                sender, him, nci->rsys, s_to_c(path));
+                reply("550 %s ... user unknown\r\n", s_to_c(path));
+                return;
+        }
+        rcpt = s_to_c(path);
+        if (!senderok(rcpt)) {
+                rejectcount++;
+                syslog(0, "smtpd", "Disallowed sending IP of %s (%s/%s) to %s",
+                                sender, him, nci->rsys, rcpt);
+                reply("550 %s ... sending system not allowed\r\n", rcpt);
+                return;
+        }
+
+        logged = 0;
+                /* forwarding() can modify 'path' on loopback request */
+        if(filterstate == ACCEPT && (fflag && !authenticated) && forwarding(path)) {
+                syslog(0, "smtpd", "Bad Forward %s (%s/%s) (%s)",
+                        s_to_c(senders.last->p), him, nci->rsys, s_to_c(path));
+                rejectcount++;
+                reply("550 we don't relay.  send to your-path@[] for loopback.\r\n");
+                return;
+        }
+        listadd(&rcvers, path);
+        reply("250 receiver is %s\r\n", s_to_c(path));
+}
+
+void
+quit(void)
+{
+        reply("221 Successful termination\r\n");
+        close(0);
+        exits(0);
+}
+
+void
+turn(void)
+{
+        if(rejectcheck())
+                return;
+        reply("502 TURN unimplemented\r\n");
+}
+
+void
+noop(void)
+{
+        if(rejectcheck())
+                return;
+        reply("250 Stop wasting my time!\r\n");
+}
+
+void
+help(String *cmd)
+{
+        if(rejectcheck())
+                return;
+        if(cmd)
+                s_free(cmd);
+        reply("250 Read rfc821 and stop wasting my time\r\n");
+}
+
+void
+verify(String *path)
+{
+        char *p, *q;
+        char *av[4];
+
+        if(rejectcheck())
+                return;
+        if(shellchars(s_to_c(path))){
+                reply("503 Bad character in address %s.\r\n", s_to_c(path));
+                return;
+        }
+        av[0] = s_to_c(mailer);
+        av[1] = "-x";
+        av[2] = s_to_c(path);
+        av[3] = 0;
+
+        pp = noshell_proc_start(av, (stream *)0, outstream(),  (stream *)0, 1, 0);
+        if (pp == 0) {
+                reply("450 We're busy right now, try later\r\n");
+                return;
+        }
+
+        p = Brdline(pp->std[1]->fp, '\n');
+        if(p == 0){
+                reply("550 String does not match anything.\r\n");
+        } else {
+                p[Blinelen(pp->std[1]->fp)-1] = 0;
+                if(strchr(p, ':'))
+                        reply("550 String does not match anything.\r\n");
+                else{
+                        q = strrchr(p, '!');
+                        if(q)
+                                p = q+1;
+                        reply("250 %s <%s@%s>\r\n", s_to_c(path), p, dom);
+                }
+        }
+        proc_wait(pp);
+        proc_free(pp);
+        pp = 0;
+}
+
+/*
+ *  get a line that ends in crnl or cr, turn terminating crnl into a nl
+ *
+ *  return 0 on EOF
+ */
+static int
+getcrnl(String *s, Biobuf *fp)
+{
+        int c;
+
+        for(;;){
+                c = Bgetc(fp);
+                if(debug) {
+                        seek(2, 0, 2);
+                        fprint(2, "%c", c);
+                }
+                switch(c){
+                case -1:
+                        goto out;
+                case '\r':
+                        c = Bgetc(fp);
+                        if(c == '\n'){
+                                if(debug) {
+                                        seek(2, 0, 2);
+                                        fprint(2, "%c", c);
+                                }
+                                s_putc(s, '\n');
+                                goto out;
+                        }
+                        Bungetc(fp);
+                        s_putc(s, '\r');
+                        break;
+                case '\n':
+                        s_putc(s, c);
+                        goto out;
+                default:
+                        s_putc(s, c);
+                        break;
+                }
+        }
+out:
+        s_terminate(s);
+        return s_len(s);
+}
+
+void
+logcall(int nbytes)
+{
+        Link *l;
+        String *to, *from;
+
+        to = s_new();
+        from = s_new();
+        for(l = senders.first; l; l = l->next){
+                if(l != senders.first)
+                        s_append(from, ", ");
+                s_append(from, s_to_c(l->p));
+        }
+        for(l = rcvers.first; l; l = l->next){
+                if(l != rcvers.first)
+                        s_append(to, ", ");
+                s_append(to, s_to_c(l->p));
+        }
+        syslog(0, "smtpd", "[%s/%s] %s sent %d bytes to %s", him, nci->rsys,
+                s_to_c(from), nbytes, s_to_c(to));
+        s_free(to);
+        s_free(from);
+}
+
+static void
+logmsg(char *action)
+{
+        Link *l;
+
+        if(logged)
+                return;
+
+        logged = 1;
+        for(l = rcvers.first; l; l = l->next)
+                syslog(0, "smtpd", "%s %s (%s/%s) (%s)", action,
+                        s_to_c(senders.last->p), him, nci->rsys, s_to_c(l->p));
+}
+
+static int
+optoutall(int filterstate)
+{
+        Link *l;
+
+        switch(filterstate){
+        case ACCEPT:
+        case TRUSTED:
+                return filterstate;
+        }
+
+        for(l = rcvers.first; l; l = l->next)
+                if(!optoutofspamfilter(s_to_c(l->p)))
+                        return filterstate;
+
+        return ACCEPT;
+}
+
+String*
+startcmd(void)
+{
+        int n;
+        Link *l;
+        char **av;
+        String *cmd;
+        char *filename;
+
+        /*
+         *  ignore the filterstate if the all the receivers prefer it.
+         */
+        filterstate = optoutall(filterstate);
+
+        switch (filterstate){
+        case BLOCKED:
+        case DELAY:
+                rejectcount++;
+                logmsg("Blocked");
+                filename = dumpfile(s_to_c(senders.last->p));
+                cmd = s_new();
+                s_append(cmd, "cat > ");
+                s_append(cmd, filename);
+                pp = proc_start(s_to_c(cmd), instream(), 0, outstream(), 0, 0);
+                break;
+        case DIALUP:
+                logmsg("Dialup");
+                rejectcount++;
+                reply("554 We don't accept mail from dial-up ports.\r\n");
+                /*
+                 * we could exit here, because we're never going to accept mail from this
+                 * ip address, but it's unclear that RFC821 allows that.  Instead we set
+                 * the hardreject flag and go stupid.
+                 */
+                hardreject = 1;
+                return 0;
+        case DENIED:
+                logmsg("Denied");
+                rejectcount++;
+                reply("554-We don't accept mail from %s.\r\n", s_to_c(senders.last->p));
+                reply("554 Contact postmaster@%s for more information.\r\n", dom);
+                return 0;
+        case REFUSED:
+                logmsg("Refused");
+                rejectcount++;
+                reply("554 Sender domain must exist: %s\r\n", s_to_c(senders.last->p));
+                return 0;
+        default:
+        case NONE:
+                logmsg("Confused");
+                rejectcount++;
+                reply("554-We have had an internal mailer error classifying your message.\r\n");
+                reply("554-Filterstate is %d\r\n", filterstate);
+                reply("554 Contact postmaster@%s for more information.\r\n", dom);
+                return 0;
+        case ACCEPT:
+        case TRUSTED:
+                /*
+                 * now that all other filters have been passed,
+                 * do grey-list processing.
+                 */
+                if(gflag)
+                        vfysenderhostok();
+
+                /*
+                 *  set up mail command
+                 */
+                cmd = s_clone(mailer);
+                n = 3;
+                for(l = rcvers.first; l; l = l->next)
+                        n++;
+                av = malloc(n*sizeof(char*));
+                if(av == nil){
+                        reply("450 We're busy right now, try later\n");
+                        s_free(cmd);
+                        return 0;
+                }
+
+                        n = 0;
+                av[n++] = s_to_c(cmd);
+                av[n++] = "-r";
+                for(l = rcvers.first; l; l = l->next)
+                        av[n++] = s_to_c(l->p);
+                av[n] = 0;
+                /*
+                 *  start mail process
+                 */
+                pp = noshell_proc_start(av, instream(), outstream(), outstream(), 0, 0);
+                free(av);
+                break;
+        }
+        if(pp == 0) {
+                reply("450 We're busy right now, try later\n");
+                s_free(cmd);
+                return 0;
+        }
+        return cmd;
+}
+
+/*
+ *  print out a header line, expanding any domainless addresses into
+ *  address@him
+ */
+char*
+bprintnode(Biobuf *b, Node *p)
+{
+        if(p->s){
+                if(p->addr && strchr(s_to_c(p->s), '@') == nil){
+                        if(Bprint(b, "%s@%s", s_to_c(p->s), him) < 0)
+                                return nil;
+                } else {
+                        if(Bwrite(b, s_to_c(p->s), s_len(p->s)) < 0)
+                                return nil;
+                }
+        }else{
+                if(Bputc(b, p->c) < 0)
+                        return nil;
+        }
+        if(p->white)
+                if(Bwrite(b, s_to_c(p->white), s_len(p->white)) < 0)
+                        return nil;
+        return p->end+1;
+}
+
+static String*
+getaddr(Node *p)
+{
+        for(; p; p = p->next)
+                if(p->s && p->addr)
+                        return p->s;
+        return nil;
+}
+
+/*
+ *  add waring headers of the form
+ *        X-warning: 
+ *  for any headers that looked like they might be forged.
+ *
+ *  return byte count of new headers
+ */
+static int
+forgedheaderwarnings(void)
+{
+        int nbytes;
+        Field *f;
+
+        nbytes = 0;
+
+        /* warn about envelope sender */
+        if(strcmp(s_to_c(senders.last->p), "/dev/null") != 0 && masquerade(senders.last->p, nil))
+                nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect envelope domain\n");
+
+        /*
+         *  check Sender: field.  If it's OK, ignore the others because this is an
+         *  exploded mailing list.
+         */
+        for(f = firstfield; f; f = f->next){
+                if(f->node->c == SENDER){
+                        if(masquerade(getaddr(f->node), him))
+                                nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect Sender: domain\n");
+                        else
+                                return nbytes;
+                }
+        }
+
+        /* check From: */
+        for(f = firstfield; f; f = f->next){
+                if(f->node->c == FROM && masquerade(getaddr(f->node), him))
+                        nbytes += Bprint(pp->std[0]->fp, "X-warning: suspect From: domain\n");
+        }
+        return nbytes;
+}
+
+/*
+ *  pipe message to mailer with the following transformations:
+ *        - change \r\n into \n.
+ *        - add sender's domain to any addrs with no domain
+ *        - add a From: if none of From:, Sender:, or Replyto: exists
+ *        - add a Received: line
+ */
+int
+pipemsg(int *byteswritten)
+{
+        int status;
+        char *cp;
+        String *line;
+        String *hdr;
+        int n, nbytes;
+        int sawdot;
+        Field *f;
+        Node *p;
+        Link *l;
+
+        pipesig(&status);        /* set status to 1 on write to closed pipe */
+        sawdot = 0;
+        status = 0;
+
+        /*
+         *  add a 'From ' line as envelope
+         */
+        nbytes = 0;
+        nbytes += Bprint(pp->std[0]->fp, "From %s %s remote from \n",
+                        s_to_c(senders.first->p), thedate());
+
+        /*
+         *  add our own Received: stamp
+         */
+        nbytes += Bprint(pp->std[0]->fp, "Received: from %s ", him);
+        if(nci->rsys)
+                nbytes += Bprint(pp->std[0]->fp, "([%s]) ", nci->rsys);
+        nbytes += Bprint(pp->std[0]->fp, "by %s; %s\n", me, thedate());
+
+        /*
+         *  read first 16k obeying '.' escape.  we're assuming
+         *  the header will all be there.
+         */
+        line = s_new();
+        hdr = s_new();
+        while(sawdot == 0 && s_len(hdr) < 16*1024){
+                n = getcrnl(s_reset(line), &bin);
+
+                /* eof or error ends the message */
+                if(n <= 0)
+                        break;
+
+                /* a line with only a '.' ends the message */
+                cp = s_to_c(line);
+                if(n == 2 && *cp == '.' && *(cp+1) == '\n'){
+                        sawdot = 1;
+                        break;
+                }
+
+                s_append(hdr, *cp == '.' ? cp+1 : cp);
+        }
+
+        /*
+          *  parse header
+         */
+        yyinit(s_to_c(hdr), s_len(hdr));
+        yyparse();
+
+        /*
+          *  Look for masquerades.  Let Sender: trump From: to allow mailing list
+         *  forwarded messages.
+         */
+        if(fflag)
+                nbytes += forgedheaderwarnings();
+
+        /*
+         *  add an orginator and/or destination if either is missing
+         */
+        if(originator == 0){
+                if(senders.last == nil)
+                        Bprint(pp->std[0]->fp, "From: /dev/null@%s\n", him);
+                else
+                        Bprint(pp->std[0]->fp, "From: %s\n", s_to_c(senders.last->p));
+        }
+        if(destination == 0){
+                Bprint(pp->std[0]->fp, "To: ");
+                for(l = rcvers.first; l; l = l->next){
+                        if(l != rcvers.first)
+                                Bprint(pp->std[0]->fp, ", ");
+                        Bprint(pp->std[0]->fp, "%s", s_to_c(l->p));
+                }
+                Bprint(pp->std[0]->fp, "\n");
+        }
+
+        /*
+         *  add sender's domain to any domainless addresses
+         *  (to avoid forging local addresses)
+         */
+        cp = s_to_c(hdr);
+        for(f = firstfield; cp != nil && f; f = f->next){
+                for(p = f->node; cp != 0 && p; p = p->next)
+                        cp = bprintnode(pp->std[0]->fp, p);
+                if(status == 0 && Bprint(pp->std[0]->fp, "\n") < 0){
+                        piperror = "write error";
+                        status = 1;
+                }
+        }
+        if(cp == nil){
+                piperror = "sender domain";
+                status = 1;
+        }
+
+        /* write anything we read following the header */
+        if(status == 0 && Bwrite(pp->std[0]->fp, cp, s_to_c(hdr) + s_len(hdr) - cp) < 0){
+                piperror = "write error 2";
+                status = 1;
+        }
+        s_free(hdr);
+
+        /*
+         *  pass rest of message to mailer.  take care of '.'
+         *  escapes.
+         */
+        while(sawdot == 0){
+                n = getcrnl(s_reset(line), &bin);
+
+                /* eof or error ends the message */
+                if(n <= 0)
+                        break;
+
+                /* a line with only a '.' ends the message */
+                cp = s_to_c(line);
+                if(n == 2 && *cp == '.' && *(cp+1) == '\n'){
+                        sawdot = 1;
+                        break;
+                }
+                nbytes += n;
+                if(status == 0 && Bwrite(pp->std[0]->fp, *cp == '.' ? cp+1 : cp, n) < 0){
+                        piperror = "write error 3";
+                        status = 1;
+                }
+        }
+        s_free(line);
+        if(sawdot == 0){
+                /* message did not terminate normally */
+                snprint(pipbuf, sizeof pipbuf, "network eof: %r");
+                piperror = pipbuf;
+                syskillpg(pp->pid);
+                status = 1;
+        }
+
+        if(status == 0 && Bflush(pp->std[0]->fp) < 0){
+                piperror = "write error 4";
+                status = 1;
+        }
+        stream_free(pp->std[0]);
+        pp->std[0] = 0;
+        *byteswritten = nbytes;
+        pipesigoff();
+        if(status && !piperror)
+                piperror = "write on closed pipe";
+        return status;
+}
+
+char*
+firstline(char *x)
+{
+        static char buf[128];
+        char *p;
+
+        strncpy(buf, x, sizeof(buf));
+        buf[sizeof(buf)-1] = 0;
+        p = strchr(buf, '\n');
+        if(p)
+                *p = 0;
+        return buf;
+}
+
+int
+sendermxcheck(void)
+{
+        char *cp, *senddom, *user;
+        char *who;
+        int pid;
+        Waitmsg *w;
+
+        who = s_to_c(senders.first->p);
+        if(strcmp(who, "/dev/null") == 0){
+                /* /dev/null can only send to one rcpt at a time */
+                if(rcvers.first != rcvers.last){
+                        werrstr("rejected: /dev/null sending to multiple recipients");
+                        return -1;
+                }
+                return 0;
+        }
+
+        if(access("/mail/lib/validatesender", AEXEC) < 0)
+                return 0;
+
+        senddom = strdup(who);
+        if((cp = strchr(senddom, '!')) == nil){
+                werrstr("rejected: domainless sender %s", who);
+                free(senddom);
+                return -1;
+        }
+        *cp++ = 0;
+        user = cp;
+
+        switch(pid = fork()){
+        case -1:
+                werrstr("deferred: fork: %r");
+                return -1;
+        case 0:
+                /*
+                 * Could add an option with the remote IP address
+                 * to allow validatesender to implement SPF eventually.
+                 */
+                execl("/mail/lib/validatesender", "validatesender", 
+                        "-n", nci->root, senddom, user, nil);
+                _exits("exec validatesender: %r");
+        default:
+                break;
+        }
+
+        free(senddom);
+        w = wait();
+        if(w == nil){
+                werrstr("deferred: wait failed: %r");
+                return -1;
+        }
+        if(w->pid != pid){
+                werrstr("deferred: wait returned wrong pid %d != %d", w->pid, pid);
+                free(w);
+                return -1;
+        }
+        if(w->msg[0] == 0){
+                free(w);
+                return 0;
+        }
+        /*
+         * skip over validatesender 143123132: prefix from rc.
+         */
+        cp = strchr(w->msg, ':');
+        if(cp && *(cp+1) == ' ')
+                werrstr("%s", cp+2);
+        else
+                werrstr("%s", w->msg);
+        free(w);
+        return -1;
+}
+
+void
+data(void)
+{
+        String *cmd;
+        String *err;
+        int status, nbytes;
+        char *cp, *ep;
+        char errx[ERRMAX];
+        Link *l;
+
+        if(rejectcheck())
+                return;
+        if(senders.last == 0){
+                reply("503 Data without MAIL FROM:\r\n");
+                rejectcount++;
+                return;
+        }
+        if(rcvers.last == 0){
+                reply("503 Data without RCPT TO:\r\n");
+                rejectcount++;
+                return;
+        }
+        if(sendermxcheck()){
+                rerrstr(errx, sizeof errx);
+                if(strncmp(errx, "rejected:", 9) == 0)
+                        reply("554 %s\r\n", errx);
+                else
+                        reply("450 %s\r\n", errx);
+                for(l=rcvers.first; l; l=l->next)
+                        syslog(0, "smtpd", "[%s/%s] %s -> %s sendercheck: %s",
+                                        him, nci->rsys, s_to_c(senders.first->p), 
+                                        s_to_c(l->p), errx);
+                rejectcount++;
+                return;
+        }
+
+        cmd = startcmd();
+        if(cmd == 0)
+                return;
+
+        reply("354 Input message; end with .\r\n");
+
+        /*
+         *  allow 145 more minutes to move the data
+         */
+        alarm(145*60*1000);
+
+        status = pipemsg(&nbytes);
+
+        /*
+         *  read any error messages
+         */
+        err = s_new();
+        while(s_read_line(pp->std[2]->fp, err))
+                ;
+
+        alarm(0);
+        atnotify(catchalarm, 0);
+
+        status |= proc_wait(pp);
+        if(debug){
+                seek(2, 0, 2);
+                fprint(2, "%d status %ux\n", getpid(), status);
+                if(*s_to_c(err))
+                        fprint(2, "%d error %s\n", getpid(), s_to_c(err));
+        }
+
+        /*
+         *  if process terminated abnormally, send back error message
+         */
+        if(status){
+                int code;
+
+                if(strstr(s_to_c(err), "mail refused")){
+                        syslog(0, "smtpd", "++[%s/%s] %s %s refused: %s", him, nci->rsys,
+                                s_to_c(senders.first->p), s_to_c(cmd), firstline(s_to_c(err)));
+                        code = 554;
+                } else {
+                        syslog(0, "smtpd", "++[%s/%s] %s %s %s%s%sreturned %#q %s", him, nci->rsys,
+                                s_to_c(senders.first->p), s_to_c(cmd), 
+                                piperror ? "error during pipemsg: " : "",
+                                piperror ? piperror : "",
+                                piperror ? "; " : "",
+                                pp->waitmsg->msg, firstline(s_to_c(err)));
+                        code = 450;
+                }
+                for(cp = s_to_c(err); ep = strchr(cp, '\n'); cp = ep){
+                        *ep++ = 0;
+                        reply("%d-%s\r\n", code, cp);
+                }
+                reply("%d mail process terminated abnormally\r\n", code);
+        } else {
+                if(filterstate == BLOCKED)
+                        reply("554 we believe this is spam.  we don't accept it.\r\n");
+                else
+                if(filterstate == DELAY)
+                        reply("554 There will be a delay in delivery of this message.\r\n");
+                else {
+                        reply("250 sent\r\n");
+                        logcall(nbytes);
+                }
+        }
+        proc_free(pp);
+        pp = 0;
+        s_free(cmd);
+        s_free(err);
+
+        listfree(&senders);
+        listfree(&rcvers);
+}
+
+/*
+ * when we have blocked a transaction based on IP address, there is nothing
+ * that the sender can do to convince us to take the message.  after the
+ * first rejection, some spammers continually RSET and give a new MAIL FROM:
+ * filling our logs with rejections.  rejectcheck() limits the retries and
+ * swiftly rejects all further commands after the first 500-series message
+ * is issued.
+ */
+int
+rejectcheck(void)
+{
+
+        if(rejectcount > MAXREJECTS){
+                syslog(0, "smtpd", "Rejected (%s/%s)", him, nci->rsys);
+                reply("554 too many errors.  transaction failed.\r\n");
+                exits("errcount");
+        }
+        if(hardreject){
+                rejectcount++;
+                reply("554 We don't accept mail from dial-up ports.\r\n");
+        }
+        return hardreject;
+}
+
+/*
+ *  create abs path of the mailer
+ */
+String*
+mailerpath(char *p)
+{
+        String *s;
+
+        if(p == nil)
+                return nil;
+        if(*p == '/')
+                return s_copy(p);
+        s = s_new();
+        s_append(s, UPASBIN);
+        s_append(s, "/");
+        s_append(s, p);
+        return s;
+}
+
+String *
+s_dec64(String *sin)
+{
+        String *sout;
+        int lin, lout;
+        lin = s_len(sin);
+
+        /*
+         * if the string is coming from smtpd.y, it will have no nl.
+         * if it is coming from getcrnl below, it will have an nl.
+         */
+        if (*(s_to_c(sin)+lin-1) == '\n')
+                lin--;
+        sout = s_newalloc(lin+1);
+        lout = dec64((uchar *)s_to_c(sout), lin, s_to_c(sin), lin);
+        if (lout < 0) {
+                s_free(sout);
+                return nil;
+        }
+        sout->ptr = sout->base + lout;
+        s_terminate(sout);
+        return sout;
+}
+
+void
+starttls(void)
+{
+        uchar *cert;
+        int certlen, fd;
+        TLSconn *conn;
+
+        conn = mallocz(sizeof *conn, 1);
+        cert = readcert(tlscert, &certlen);
+        if (conn == nil || cert == nil) {
+                if (conn != nil)
+                        free(conn);
+                reply("454 TLS not available\r\n");
+                return;
+        }
+        reply("220 Go ahead make my day\r\n");
+        conn->cert = cert;
+        conn->certlen = certlen;
+        fd = tlsServer(Bfildes(&bin), conn);
+        if (fd < 0) {
+                free(cert);
+                free(conn);
+                syslog(0, "smtpd", "TLS start-up failed with %s", him);
+
+                /* force the client to hang up */
+                close(Bfildes(&bin));                /* probably fd 0 */
+                close(1);
+                exits("tls failed");
+        }
+        Bterm(&bin);
+        Binit(&bin, fd, OREAD);
+        if (dup(fd, 1) < 0)
+                fprint(2, "dup of %d failed: %r\n", fd);
+        passwordinclear = 1;
+        syslog(0, "smtpd", "started TLS with %s", him);
+}
+
+void
+auth(String *mech, String *resp)
+{
+        Chalstate *chs = nil;
+        AuthInfo *ai = nil;
+        String *s_resp1_64 = nil;
+        String *s_resp2_64 = nil;
+        String *s_resp1 = nil;
+        String *s_resp2 = nil;
+        char *scratch = nil;
+        char *user, *pass;
+
+        if (rejectcheck())
+                goto bomb_out;
+
+         syslog(0, "smtpd", "auth(%s, %s) from %s", s_to_c(mech),
+                "(protected)", him);
+
+        if (authenticated) {
+        bad_sequence:
+                rejectcount++;
+                reply("503 Bad sequence of commands\r\n");
+                goto bomb_out;
+        }
+        if (cistrcmp(s_to_c(mech), "plain") == 0) {
+
+                if (!passwordinclear) {
+                        rejectcount++;
+                        reply("538 Encryption required for requested authentication mechanism\r\n");
+                        goto bomb_out;
+                }
+                s_resp1_64 = resp;
+                if (s_resp1_64 == nil) {
+                        reply("334 \r\n");
+                        s_resp1_64 = s_new();
+                        if (getcrnl(s_resp1_64, &bin) <= 0) {
+                                goto bad_sequence;
+                        }
+                }
+                s_resp1 = s_dec64(s_resp1_64);
+                if (s_resp1 == nil) {
+                        rejectcount++;
+                        reply("501 Cannot decode base64\r\n");
+                        goto bomb_out;
+                }
+                memset(s_to_c(s_resp1_64), 'X', s_len(s_resp1_64));
+                user = (s_to_c(s_resp1) + strlen(s_to_c(s_resp1)) + 1);
+                pass = user + (strlen(user) + 1);
+                ai = auth_userpasswd(user, pass);
+                authenticated = ai != nil;
+                memset(pass, 'X', strlen(pass));
+                goto windup;
+        }
+        else if (cistrcmp(s_to_c(mech), "login") == 0) {
+
+                if (!passwordinclear) {
+                        rejectcount++;
+                        reply("538 Encryption required for requested authentication mechanism\r\n");
+                        goto bomb_out;
+                }
+                if (resp == nil) {
+                        reply("334 VXNlcm5hbWU6\r\n");
+                        s_resp1_64 = s_new();
+                        if (getcrnl(s_resp1_64, &bin) <= 0)
+                                goto bad_sequence;
+                }
+                reply("334 UGFzc3dvcmQ6\r\n");
+                s_resp2_64 = s_new();
+                if (getcrnl(s_resp2_64, &bin) <= 0)
+                        goto bad_sequence;
+                s_resp1 = s_dec64(s_resp1_64);
+                s_resp2 = s_dec64(s_resp2_64);
+                memset(s_to_c(s_resp2_64), 'X', s_len(s_resp2_64));
+                if (s_resp1 == nil || s_resp2 == nil) {
+                        rejectcount++;
+                        reply("501 Cannot decode base64\r\n");
+                        goto bomb_out;
+                }
+                ai = auth_userpasswd(s_to_c(s_resp1), s_to_c(s_resp2));
+                authenticated = ai != nil;
+                memset(s_to_c(s_resp2), 'X', s_len(s_resp2));
+        windup:
+                if (authenticated)
+                        reply("235 Authentication successful\r\n");
+                else {
+                        rejectcount++;
+                        reply("535 Authentication failed\r\n");
+                }
+                goto bomb_out;
+        }
+        else if (cistrcmp(s_to_c(mech), "cram-md5") == 0) {
+                char *resp;
+                int chal64n;
+                char *t;
+
+                chs = auth_challenge("proto=cram role=server");
+                if (chs == nil) {
+                        rejectcount++;
+                        reply("501 Couldn't get CRAM-MD5 challenge\r\n");
+                        goto bomb_out;
+                }
+                scratch = malloc(chs->nchal * 2 + 1);
+                chal64n = enc64(scratch, chs->nchal * 2, (uchar *)chs->chal, chs->nchal);
+                scratch[chal64n] = 0;
+                reply("334 %s\r\n", scratch);
+                s_resp1_64 = s_new();
+                if (getcrnl(s_resp1_64, &bin) <= 0)
+                        goto bad_sequence;
+                s_resp1 = s_dec64(s_resp1_64);
+                if (s_resp1 == nil) {
+                        rejectcount++;
+                        reply("501 Cannot decode base64\r\n");
+                        goto bomb_out;
+                }
+                /* should be of form  */
+                resp = s_to_c(s_resp1);
+                t = strchr(resp, ' ');
+                if (t == nil) {
+                        rejectcount++;
+                        reply("501 Poorly formed CRAM-MD5 response\r\n");
+                        goto bomb_out;
+                }
+                *t++ = 0;
+                chs->user = resp;
+                chs->resp = t;
+                chs->nresp = strlen(t);
+                ai = auth_response(chs);
+                authenticated = ai != nil;
+                goto windup;
+        }
+        rejectcount++;
+        reply("501 Unrecognised authentication type %s\r\n", s_to_c(mech));
+bomb_out:
+        if (ai)
+                auth_freeAI(ai);
+        if (chs)
+                auth_freechal(chs);
+        if (scratch)
+                free(scratch);
+        if (s_resp1)
+                s_free(s_resp1);
+        if (s_resp2)
+                s_free(s_resp2);
+        if (s_resp1_64)
+                s_free(s_resp1_64);
+        if (s_resp2_64)
+                s_free(s_resp2_64);
+}
diff --git a/src/cmd/upas/smtp/smtpd.h b/src/cmd/upas/smtp/smtpd.h
t@@ -0,0 +1,68 @@
+enum {
+        ACCEPT = 0,
+        REFUSED,
+        DENIED,
+        DIALUP,
+        BLOCKED,
+        DELAY,
+        TRUSTED,
+        NONE,
+
+        MAXREJECTS = 100,
+};
+
+        
+typedef struct Link Link;
+typedef struct List List;
+
+struct Link {
+        Link *next;
+        String *p;
+};
+
+struct List {
+        Link *first;
+        Link *last;
+};
+
+extern        int        fflag;
+extern        int        rflag;
+extern        int        sflag;
+
+extern        int        debug;
+extern        NetConnInfo        *nci;
+extern        char        *dom;
+extern        char*        me;
+extern        int        trusted;
+extern        List        senders;
+extern        List        rcvers;
+
+void        addbadguy(char*);
+void        auth(String *, String *);
+int        blocked(String*);
+void        data(void);
+char*        dumpfile(char*);
+int        forwarding(String*);
+void        getconf(void);
+void        hello(String*, int extended);
+void        help(String *);
+int        isbadguy(void);
+void        listadd(List*, String*);
+void        listfree(List*);
+int        masquerade(String*, char*);
+void        noop(void);
+int        optoutofspamfilter(char*);
+void        quit(void);
+void        parseinit(void);
+void        receiver(String*);
+int        recipok(char*);
+int        reply(char*, ...);
+void        reset(void);
+int        rmtdns(char*, char*);
+void        sayhi(void);
+void        sender(String*);
+void        starttls(void);
+void        turn(void);
+void        verify(String*);
+void        vfysenderhostok(void);
+int        zzparse(void);
diff --git a/src/cmd/upas/smtp/smtpd.y b/src/cmd/upas/smtp/smtpd.y
t@@ -0,0 +1,317 @@
+%{
+#include "common.h"
+#include 
+#include "smtpd.h"
+
+#define YYSTYPE yystype
+typedef struct quux yystype;
+struct quux {
+        String        *s;
+        int        c;
+};
+Biobuf *yyfp;
+YYSTYPE *bang;
+extern Biobuf bin;
+extern int debug;
+
+YYSTYPE cat(YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*, YYSTYPE*);
+int yyparse(void);
+int yylex(void);
+YYSTYPE anonymous(void);
+%}
+
+%term SPACE
+%term CNTRL
+%term CRLF
+%start conversation
+%%
+
+conversation        : cmd
+                | conversation cmd
+                ;
+cmd                : error
+                | 'h' 'e' 'l' 'o' spaces sdomain CRLF
+                        { hello($6.s, 0); }
+                | 'e' 'h' 'l' 'o' spaces sdomain CRLF
+                        { hello($6.s, 1); }
+                | 'm' 'a' 'i' 'l' spaces 'f' 'r' 'o' 'm' ':' spath CRLF
+                        { sender($11.s); }
+                | 'm' 'a' 'i' 'l' spaces 'f' 'r' 'o' 'm' ':' spath spaces 'a' 'u' 't' 'h' '=' sauth CRLF
+                        { sender($11.s); }
+                | 'r' 'c' 'p' 't' spaces 't' 'o' ':' spath CRLF
+                        { receiver($9.s); }
+                | 'd' 'a' 't' 'a' CRLF
+                        { data(); }
+                | 'r' 's' 'e' 't' CRLF
+                        { reset(); }
+                | 's' 'e' 'n' 'd' spaces 'f' 'r' 'o' 'm' ':' spath CRLF
+                        { sender($11.s); }
+                | 's' 'o' 'm' 'l' spaces 'f' 'r' 'o' 'm'  ':' spath CRLF
+                        { sender($11.s); }
+                | 's' 'a' 'm' 'l' spaces 'f' 'r' 'o' 'm' ':' spath CRLF
+                        { sender($11.s); }
+                | 'v' 'r' 'f' 'y' spaces string CRLF
+                        { verify($6.s); }
+                | 'e' 'x' 'p' 'n' spaces string CRLF
+                        { verify($6.s); }
+                | 'h' 'e' 'l' 'p' CRLF
+                        { help(0); }
+                | 'h' 'e' 'l' 'p' spaces string CRLF
+                        { help($6.s); }
+                | 'n' 'o' 'o' 'p' CRLF
+                        { noop(); }
+                | 'q' 'u' 'i' 't' CRLF
+                        { quit(); }
+                | 't' 'u' 'r' 'n' CRLF
+                        { turn(); }
+                | 's' 't' 'a' 'r' 't' 't' 'l' 's' CRLF
+                        { starttls(); }
+                | 'a' 'u' 't' 'h' spaces name spaces string CRLF
+                        { auth($6.s, $8.s); }
+                | 'a' 'u' 't' 'h' spaces name CRLF
+                        { auth($6.s, nil); }
+                | CRLF
+                        { reply("501 illegal command or bad syntax\r\n"); }
+                ;
+path                : '<' '>'                        ={ $$ = anonymous(); }
+                | '<' mailbox '>'                ={ $$ = $2; }
+                | '<' a_d_l ':' mailbox '>'        ={ $$ = cat(&$2, bang, &$4, 0, 0 ,0, 0); }
+                ;
+spath                : path                        ={ $$ = $1; }
+                | spaces path                ={ $$ = $2; }
+                ;
+auth                : path                        ={ $$ = $1; }
+                | mailbox                ={ $$ = $1; }
+                ;
+sauth                : auth                        ={ $$ = $1; }
+                | spaces auth                ={ $$ = $2; }
+                ;
+                ;
+a_d_l                : at_domain                ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | at_domain ',' a_d_l        ={ $$ = cat(&$1, bang, &$3, 0, 0, 0, 0); }
+                ;
+at_domain        : '@' domain                ={ $$ = cat(&$2, 0, 0, 0, 0 ,0, 0); }
+                ;
+sdomain                : domain                ={ $$ = $1; }
+                | domain spaces                ={ $$ = $1; }
+                ;
+domain                : element                ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | element '.'                ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | element '.' domain        ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+                ;
+element                : name                        ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | '#' number                ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                | '[' ']'                ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                | '[' dotnum ']'        ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+                ;
+mailbox                : local_part                ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | local_part '@' domain        ={ $$ = cat(&$3, bang, &$1, 0, 0 ,0, 0); }
+                ;
+local_part        : dot_string                ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | quoted_string                ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                ;
+name                : let_dig                        ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | let_dig ld_str                ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                | let_dig ldh_str ld_str        ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+                ;
+ld_str                : let_dig
+                | let_dig ld_str                ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                ;
+ldh_str                : hunder
+                | ld_str hunder                ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                | ldh_str ld_str hunder        ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+                ;
+let_dig                : a
+                | d
+                ;
+dot_string        : string                        ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | string '.' dot_string                ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+                ;
+
+string                : char        ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | string char        ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                ;
+
+quoted_string        : '"' qtext '"'        ={ $$ = cat(&$1, &$2, &$3, 0, 0 ,0, 0); }
+                ;
+qtext                : '\\' x                ={ $$ = cat(&$2, 0, 0, 0, 0 ,0, 0); }
+                | qtext '\\' x                ={ $$ = cat(&$1, &$3, 0, 0, 0 ,0, 0); }
+                | q
+                | qtext q                ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                ;
+char                : c
+                | '\\' x                ={ $$ = $2; }
+                ;
+dotnum                : snum '.' snum '.' snum '.' snum ={ $$ = cat(&$1, &$2, &$3, &$4, &$5, &$6, &$7); }
+                ;
+number                : d                ={ $$ = cat(&$1, 0, 0, 0, 0 ,0, 0); }
+                | number d        ={ $$ = cat(&$1, &$2, 0, 0, 0 ,0, 0); }
+                ;
+snum                : number                ={ if(atoi(s_to_c($1.s)) > 255) print("bad snum\n"); } 
+                ;
+spaces                : SPACE                ={ $$ = $1; }
+                | SPACE        spaces        ={ $$ = $1; }
+                ;
+hunder                : '-' | '_'
+                ;
+special1        : CNTRL
+                | '(' | ')' | ',' | '.'
+                | ':' | ';' | '<' | '>' | '@'
+                ;
+special                : special1 | '\\' | '"'
+                ;
+notspecial        : '!' | '#' | '$' | '%' | '&' | '\''
+                | '*' | '+' | '-' | '/'
+                | '=' | '?'
+                | '[' | ']' | '^' | '_' | '`' | '{' | '|' | '}' | '~'
+                ;
+
+a                : 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i'
+                | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r'
+                | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
+                ;
+d                : '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
+                ;
+c                : a | d | notspecial
+                ;                
+q                : a | d | special1 | notspecial | SPACE
+                ;
+x                : a | d | special | notspecial | SPACE
+                ;
+%%
+
+void
+parseinit(void)
+{
+        bang = (YYSTYPE*)malloc(sizeof(YYSTYPE));
+        bang->c = '!';
+        bang->s = 0;
+        yyfp = &bin;
+}
+
+yylex(void)
+{
+        int c;
+
+        for(;;){
+                c = Bgetc(yyfp);
+                if(c == -1)
+                        return 0;
+                if(debug)
+                        fprint(2, "%c", c);
+                yylval.c = c = c & 0x7F;
+                if(c == '\n'){
+                        return CRLF;
+                }
+                if(c == '\r'){
+                        c = Bgetc(yyfp);
+                        if(c != '\n'){
+                                Bungetc(yyfp);
+                                c = '\r';
+                        } else {
+                                if(debug)
+                                        fprint(2, "%c", c);
+                                return CRLF;
+                        }
+                }
+                if(isalpha(c))
+                        return tolower(c);
+                if(isspace(c))
+                        return SPACE;
+                if(iscntrl(c))
+                        return CNTRL;
+                return c;
+        }
+}
+
+YYSTYPE
+cat(YYSTYPE *y1, YYSTYPE *y2, YYSTYPE *y3, YYSTYPE *y4, YYSTYPE *y5, YYSTYPE *y6, YYSTYPE *y7)
+{
+        YYSTYPE rv;
+
+        if(y1->s)
+                rv.s = y1->s;
+        else {
+                rv.s = s_new();
+                s_putc(rv.s, y1->c);
+                s_terminate(rv.s);
+        }
+        if(y2){
+                if(y2->s){
+                        s_append(rv.s, s_to_c(y2->s));
+                        s_free(y2->s);
+                } else {
+                        s_putc(rv.s, y2->c);
+                        s_terminate(rv.s);
+                }
+        } else
+                return rv;
+        if(y3){
+                if(y3->s){
+                        s_append(rv.s, s_to_c(y3->s));
+                        s_free(y3->s);
+                } else {
+                        s_putc(rv.s, y3->c);
+                        s_terminate(rv.s);
+                }
+        } else
+                return rv;
+        if(y4){
+                if(y4->s){
+                        s_append(rv.s, s_to_c(y4->s));
+                        s_free(y4->s);
+                } else {
+                        s_putc(rv.s, y4->c);
+                        s_terminate(rv.s);
+                }
+        } else
+                return rv;
+        if(y5){
+                if(y5->s){
+                        s_append(rv.s, s_to_c(y5->s));
+                        s_free(y5->s);
+                } else {
+                        s_putc(rv.s, y5->c);
+                        s_terminate(rv.s);
+                }
+        } else
+                return rv;
+        if(y6){
+                if(y6->s){
+                        s_append(rv.s, s_to_c(y6->s));
+                        s_free(y6->s);
+                } else {
+                        s_putc(rv.s, y6->c);
+                        s_terminate(rv.s);
+                }
+        } else
+                return rv;
+        if(y7){
+                if(y7->s){
+                        s_append(rv.s, s_to_c(y7->s));
+                        s_free(y7->s);
+                } else {
+                        s_putc(rv.s, y7->c);
+                        s_terminate(rv.s);
+                }
+        } else
+                return rv;
+}
+
+void
+yyerror(char *x)
+{
+        USED(x);
+}
+
+/*
+ *  an anonymous user
+ */
+YYSTYPE
+anonymous(void)
+{
+        YYSTYPE rv;
+
+        rv.s = s_copy("/dev/null");
+        return rv;
+}
diff --git a/src/cmd/upas/smtp/spam.c b/src/cmd/upas/smtp/spam.c
t@@ -0,0 +1,591 @@
+#include "common.h"
+#include "smtpd.h"
+#include 
+
+enum {
+        NORELAY = 0,
+        DNSVERIFY,
+        SAVEBLOCK,
+        DOMNAME,
+        OURNETS,
+        OURDOMS,
+
+        IP = 0,
+        STRING,
+};
+
+
+typedef struct Keyword Keyword;
+
+struct Keyword {
+        char        *name;
+        int        code;
+};
+
+static Keyword options[] = {
+        "norelay",                NORELAY,
+        "verifysenderdom",        DNSVERIFY,
+        "saveblockedmsg",        SAVEBLOCK,
+        "defaultdomain",        DOMNAME,        
+        "ournets",                OURNETS,
+        "ourdomains",                OURDOMS,
+        0,                        NONE,
+};
+
+static Keyword actions[] = {
+        "allow",                ACCEPT,
+        "block",                BLOCKED,
+        "deny",                        DENIED,
+        "dial",                        DIALUP,
+        "delay",                DELAY,
+        0,                        NONE,
+};
+
+static        int        hisaction;
+static        List        ourdoms;
+static        List         badguys;
+static        ulong        v4peerip;
+
+static        char*        getline(Biobuf*);
+static        int        cidrcheck(char*);
+
+static int
+findkey(char *val, Keyword *p)
+{
+
+        for(; p->name; p++)
+                if(strcmp(val, p->name) == 0)
+                                break;
+        return p->code;
+}
+
+char*
+actstr(int a)
+{
+        char buf[32];
+        Keyword *p;
+
+        for(p=actions; p->name; p++)
+                if(p->code == a)
+                        return p->name;
+        if(a==NONE)
+                return "none";
+        sprint(buf, "%d", a);
+        return buf;
+}
+
+int
+getaction(char *s, char *type)
+{
+        char buf[1024];
+        Keyword *k;
+
+        if(s == nil || *s == 0)
+                return ACCEPT;
+
+        for(k = actions; k->name != 0; k++){
+                snprint(buf, sizeof buf, "/mail/ratify/%s/%s/%s", k->name, type, s);
+                if(access(buf,0) >= 0)
+                        return k->code;
+        }
+        return ACCEPT;
+}
+
+int
+istrusted(char *s)
+{
+        char buf[1024];
+
+        if(s == nil || *s == 0)
+                return 0;
+
+        snprint(buf, sizeof buf, "/mail/ratify/trusted/%s", s);
+        return access(buf,0) >= 0;
+}
+
+void
+getconf(void)
+{
+        Biobuf *bp;
+        char *cp, *p;
+        String *s;
+        char buf[512];
+        uchar addr[4];
+
+        v4parseip(addr, nci->rsys);
+        v4peerip = nhgetl(addr);
+
+        trusted = istrusted(nci->rsys);
+        hisaction = getaction(nci->rsys, "ip");
+        if(debug){
+                fprint(2, "istrusted(%s)=%d\n", nci->rsys, trusted);
+                fprint(2, "getaction(%s, ip)=%s\n", nci->rsys, actstr(hisaction));
+        }
+        snprint(buf, sizeof(buf), "%s/smtpd.conf", UPASLIB);
+        bp = sysopen(buf, "r", 0);
+        if(bp == 0)
+                return;
+
+        for(;;){
+                cp = getline(bp);
+                if(cp == 0)
+                        break;
+                p = cp+strlen(cp)+1;
+                switch(findkey(cp, options)){
+                case NORELAY:
+                        if(fflag == 0 && strcmp(p, "on") == 0)
+                                fflag++;
+                        break;
+                case DNSVERIFY:
+                        if(rflag == 0 && strcmp(p, "on") == 0)
+                                rflag++;
+                        break;
+                case SAVEBLOCK:
+                        if(sflag == 0 && strcmp(p, "on") == 0)
+                                sflag++;
+                        break;
+                case DOMNAME:
+                        if(dom == 0)
+                                dom = strdup(p);
+                        break;
+                case OURNETS:
+                        if (trusted == 0)
+                                trusted = cidrcheck(p);
+                        break;
+                case OURDOMS:
+                        while(*p){
+                                s = s_new();
+                                s_append(s, p);
+                                listadd(&ourdoms, s);
+                                p += strlen(p)+1;
+                        }
+                        break;
+                default:
+                        break;
+                }
+        }
+        sysclose(bp);
+}
+
+/*
+ *        match a user name.  the only meta-char is '*' which matches all
+ *        characters.  we only allow it as "*", which matches anything or
+ *        an * at the end of the name (e.g., "username*") which matches
+ *        trailing characters.
+ */
+static int
+usermatch(char *pathuser, char *specuser)
+{
+        int n;
+
+        n = strlen(specuser)-1;
+        if(specuser[n] == '*'){
+                if(n == 0)                /* match everything */
+                        return 0;
+                return strncmp(pathuser, specuser, n);
+        }
+        return strcmp(pathuser, specuser);
+}
+
+static int
+dommatch(char *pathdom, char *specdom)
+{
+        int n;
+
+        if (*specdom == '*'){
+                if (specdom[1] == '.' && specdom[2]){
+                        specdom += 2;
+                        n = strlen(pathdom)-strlen(specdom);
+                        if(n == 0 || (n > 0 && pathdom[n-1] == '.'))
+                                return strcmp(pathdom+n, specdom);
+                        return n;
+                }
+        }
+        return strcmp(pathdom, specdom);
+}
+
+/*
+ *  figure out action for this sender
+ */
+int
+blocked(String *path)
+{
+        String *lpath;
+        int action;
+
+        if(debug)
+                fprint(2, "blocked(%s)\n", s_to_c(path));
+
+        /* if the sender's IP address is blessed, ignore sender email address */
+        if(trusted){
+                if(debug)
+                        fprint(2, "\ttrusted => trusted\n");
+                return TRUSTED;
+        }
+
+        /* if sender's IP address is blocked, ignore sender email address */
+        if(hisaction != ACCEPT){
+                if(debug)
+                        fprint(2, "\thisaction=%s => %s\n", actstr(hisaction), actstr(hisaction));
+                return hisaction;
+        }
+
+        /* convert to lower case */
+        lpath = s_copy(s_to_c(path));
+        s_tolower(lpath);
+
+        /* classify */
+        action = getaction(s_to_c(lpath), "account");
+        if(debug)
+                fprint(2, "\tgetaction account %s => %s\n", s_to_c(lpath), actstr(action));
+        s_free(lpath);
+        return action;
+}
+
+/*
+ * get a canonicalized line: a string of null-terminated lower-case
+ * tokens with a two null bytes at the end.
+ */
+static char*
+getline(Biobuf *bp)
+{
+        char c, *cp, *p, *q;
+        int n;
+
+        static char *buf;
+        static int bufsize;
+
+        for(;;){
+                cp = Brdline(bp, '\n');
+                if(cp == 0)
+                        return 0;
+                n = Blinelen(bp);
+                cp[n-1] = 0;
+                if(buf == 0 || bufsize < n+1){
+                        bufsize += 512;
+                        if(bufsize < n+1)
+                                bufsize = n+1;
+                        buf = realloc(buf, bufsize);
+                        if(buf == 0)
+                                break;
+                }
+                q = buf;
+                for (p = cp; *p; p++){
+                        c = *p;
+                        if(c == '\\' && p[1])        /* we don't allow \ */
+                                c = *++p;
+                        else
+                        if(c == '#')
+                                break;
+                        else
+                        if(c == ' ' || c == '\t' || c == ',')
+                                if(q == buf || q[-1] == 0)
+                                        continue;
+                                else
+                                        c = 0;
+                        *q++ = tolower(c);
+                }
+                if(q != buf){
+                        if(q[-1])
+                                *q++ = 0;
+                        *q = 0;
+                        break;
+                }
+        }
+        return buf;
+}
+
+static int
+isourdom(char *s)
+{
+        Link *l;
+
+        if(strchr(s, '.') == nil)
+                return 1;
+
+        for(l = ourdoms.first; l; l = l->next){
+                if(dommatch(s, s_to_c(l->p)) == 0)
+                        return 1;
+        }
+        return 0;
+}
+
+int
+forwarding(String *path)
+{
+        char *cp, *s;
+        String *lpath;
+
+        if(debug)
+                fprint(2, "forwarding(%s)\n", s_to_c(path));
+
+        /* first check if they want loopback */
+        lpath = s_copy(s_to_c(s_restart(path)));
+        if(nci->rsys && *nci->rsys){
+                cp = s_to_c(lpath);
+                if(strncmp(cp, "[]!", 3) == 0){
+found:
+                        s_append(path, "[");
+                        s_append(path, nci->rsys);
+                        s_append(path, "]!");
+                        s_append(path, cp+3);
+                        s_terminate(path);
+                        s_free(lpath);
+                        return 0;
+                }
+                cp = strchr(cp,'!');                        /* skip our domain and check next */
+                if(cp++ && strncmp(cp, "[]!", 3) == 0)
+                        goto found;
+        }
+
+        /* if mail is from a trusted IP addr, allow it to forward */
+        if(trusted) {
+                s_free(lpath);
+                return 0;
+        }
+
+        /* sender is untrusted; ensure receiver is in one of our domains */
+        for(cp = s_to_c(lpath); *cp; cp++)                /* convert receiver lc */
+                *cp = tolower(*cp);
+
+        for(s = s_to_c(lpath); cp = strchr(s, '!'); s = cp+1){
+                *cp = 0;
+                if(!isourdom(s)){
+                        s_free(lpath);
+                        return 1;
+                }
+        }
+        s_free(lpath);
+        return 0;
+}
+
+int
+masquerade(String *path, char *him)
+{
+        char *cp, *s;
+        String *lpath;
+        int rv = 0;
+
+        if(debug)
+                fprint(2, "masquerade(%s)\n", s_to_c(path));
+
+        if(trusted)
+                return 0;
+        if(path == nil)
+                return 0;
+
+        lpath = s_copy(s_to_c(path));
+
+        /* sender is untrusted; ensure receiver is in one of our domains */
+        for(cp = s_to_c(lpath); *cp; cp++)                /* convert receiver lc */
+                *cp = tolower(*cp);
+        s = s_to_c(lpath);
+
+        /* scan first element of ! or last element of @ paths */
+        if((cp = strchr(s, '!')) != nil){
+                *cp = 0;
+                if(isourdom(s))
+                        rv = 1;
+        } else if((cp = strrchr(s, '@')) != nil){
+                if(isourdom(cp+1))
+                        rv = 1;
+        } else {
+                if(isourdom(him))
+                        rv = 1;
+        }
+
+        s_free(lpath);
+        return rv;
+}
+
+/* this is a v4 only check */
+static int
+cidrcheck(char *cp)
+{
+        char *p;
+        ulong a, m;
+        uchar addr[IPv4addrlen];
+        uchar mask[IPv4addrlen];
+
+        if(v4peerip == 0)
+                return 0;
+
+        /* parse a list of CIDR addresses comparing each to the peer IP addr */
+        while(cp && *cp){
+                v4parsecidr(addr, mask, cp);
+                a = nhgetl(addr);
+                m = nhgetl(mask);
+                /*
+                 * if a mask isn't specified, we build a minimal mask
+                 * instead of using the default mask for that net.  in this
+                 * case we never allow a class A mask (0xff000000).
+                 */
+                if(strchr(cp, '/') == 0){
+                        m = 0xff000000;
+                        p = cp;
+                        for(p = strchr(p, '.'); p && p[1]; p = strchr(p+1, '.'))
+                                        m = (m>>8)|0xff000000;
+
+                        /* force at least a class B */
+                        m |= 0xffff0000;
+                }
+                if((v4peerip&m) == a)
+                        return 1;
+                cp += strlen(cp)+1;
+        }                
+        return 0;
+}
+
+int
+isbadguy(void)
+{
+        Link *l;
+
+        /* check if this IP address is banned */
+        for(l = badguys.first; l; l = l->next)
+                if(cidrcheck(s_to_c(l->p)))
+                        return 1;
+
+        return 0;
+}
+
+void
+addbadguy(char *p)
+{
+        listadd(&badguys, s_copy(p));
+};
+
+char*
+dumpfile(char *sender)
+{
+        int i, fd;
+        ulong h;
+        static char buf[512];
+        char *cp;
+
+        if (sflag == 1){
+                cp = ctime(time(0));
+                cp[7] = 0;
+                if(cp[8] == ' ')
+                        sprint(buf, "%s/queue.dump/%s%c", SPOOL, cp+4, cp[9]);
+                else
+                        sprint(buf, "%s/queue.dump/%s%c%c", SPOOL, cp+4, cp[8], cp[9]);
+                cp = buf+strlen(buf);
+                if(access(buf, 0) < 0 && sysmkdir(buf, 0777) < 0)
+                        return "/dev/null";
+                h = 0;
+                while(*sender)
+                        h = h*257 + *sender++;
+                for(i = 0; i < 50; i++){
+                        h += lrand();
+                        sprint(cp, "/%lud", h);
+                        if(access(buf, 0) >= 0)
+                                continue;
+                        fd = syscreate(buf, ORDWR, 0666);
+                        if(fd >= 0){
+                                if(debug)
+                                        fprint(2, "saving in %s\n", buf);
+                                close(fd);
+                                return buf;
+                        }
+                }
+        }
+        return "/dev/null";
+}
+
+char *validator = "/mail/lib/validateaddress";
+
+int
+recipok(char *user)
+{
+        char *cp, *p, c;
+        char buf[512];
+        int n;
+        Biobuf *bp;
+        int pid;
+        Waitmsg *w;
+
+        if(shellchars(user)){
+                syslog(0, "smtpd", "shellchars in user name");
+                return 0;
+        }
+
+        if(access(validator, AEXEC) == 0)
+        switch(pid = fork()) {
+        case -1:
+                break;
+        case 0:
+                execl(validator, "validateaddress", user, nil);
+                exits(0);
+        default:
+                while(w = wait()) {
+                        if(w->pid != pid)
+                                continue;
+                        if(w->msg[0] != 0){
+                                /*
+                                syslog(0, "smtpd", "validateaddress %s: %s", user, w->msg);
+                                */
+                                return 0;
+                        }
+                        break;
+                }
+        }
+
+        snprint(buf, sizeof(buf), "%s/names.blocked", UPASLIB);
+        bp = sysopen(buf, "r", 0);
+        if(bp == 0)
+                return 1;
+        for(;;){
+                cp = Brdline(bp, '\n');
+                if(cp == 0)
+                        break;
+                n = Blinelen(bp);
+                cp[n-1] = 0;
+
+                while(*cp == ' ' || *cp == '\t')
+                        cp++;
+                for(p = cp; c = *p; p++){
+                        if(c == '#')
+                                break;
+                        if(c == ' ' || c == '\t')
+                                break;
+                }
+                if(p > cp){
+                        *p = 0;
+                        if(cistrcmp(user, cp) == 0){
+                                syslog(0, "smtpd", "names.blocked blocks %s", user);
+                                Bterm(bp);
+                                return 0;
+                        }
+                }
+        }
+        Bterm(bp);
+        return 1;
+}
+
+/*
+ *  a user can opt out of spam filtering by creating
+ *  a file in his mail directory named 'nospamfiltering'.
+ */
+int
+optoutofspamfilter(char *addr)
+{
+        char *p, *f;
+        int rv;
+
+        p = strchr(addr, '!');
+        if(p)
+                p++;
+        else
+                p = addr;
+
+
+        rv = 0;
+        f = smprint("/mail/box/%s/nospamfiltering", p);
+        if(f != nil){
+                rv = access(f, 0)==0;
+                free(f);
+        }
+
+        return rv;
+}
diff --git a/src/cmd/upas/smtp/y.tab.h b/src/cmd/upas/smtp/y.tab.h
t@@ -0,0 +1,25 @@
+#define        WORD        57346
+#define        DATE        57347
+#define        RESENT_DATE        57348
+#define        RETURN_PATH        57349
+#define        FROM        57350
+#define        SENDER        57351
+#define        REPLY_TO        57352
+#define        RESENT_FROM        57353
+#define        RESENT_SENDER        57354
+#define        RESENT_REPLY_TO        57355
+#define        SUBJECT        57356
+#define        TO        57357
+#define        CC        57358
+#define        BCC        57359
+#define        RESENT_TO        57360
+#define        RESENT_CC        57361
+#define        RESENT_BCC        57362
+#define        REMOTE        57363
+#define        PRECEDENCE        57364
+#define        MIMEVERSION        57365
+#define        CONTENTTYPE        57366
+#define        MESSAGEID        57367
+#define        RECEIVED        57368
+#define        MAILER        57369
+#define        BADTOKEN        57370
diff --git a/src/cmd/upas/unesc/mkfile b/src/cmd/upas/unesc/mkfile
t@@ -0,0 +1,17 @@
+
diff --git a/src/cmd/upas/unesc/unesc.c b/src/cmd/upas/unesc/unesc.c
t@@ -0,0 +1,48 @@
+/*
+ *        upas/unesc - interpret =?foo?bar?=char?= escapes
+ */
+
+#include 
+#include 
+
+int
+hex(int c)
+{
+        if('0' <= c && c <= '9')
+                return c - '0';
+        if('A' <= c && c <= 'F')
+                return c - 'A' + 10;
+        if('a' <= c && c <= 'f')
+                return c - 'a' + 10;
+        return 0;
+}
+
+void
+main(int argc, char **argv)
+{
+        int c;
+
+        while((c=getchar()) != EOF){
+                if(c == '='){
+                        if((c=getchar()) == '?'){
+                                while((c=getchar()) != EOF && c != '?')
+                                        continue;
+                                while((c=getchar()) != EOF && c != '?')
+                                        continue;
+                                while((c=getchar()) != EOF && c != '?'){
+                                        if(c == '='){
+                                                c = hex(getchar()) << 4;
+                                                c |= hex(getchar());
+                                        }
+                                        putchar(c);
+                                }
+                                (void) getchar();        /* consume '=' */
+                        }else{
+                                putchar('=');
+                                putchar(c);
+                        }
+                }else
+                        putchar(c);
+        }
+        exit(0);
+}
diff --git a/src/cmd/upas/vf/mkfile b/src/cmd/upas/vf/mkfile
t@@ -0,0 +1,20 @@
+<$PLAN9/src/mkhdr
+
+TARG=vf
+
+OFILES=vf.$O\
+
+LIB=../common/libcommon.a\
+
+HFILES=../common/common.h\
+         ../common/sys.h\
+
+
+BIN=$PLAN9/bin/upas
+UPDATE=\
+        mkfile\
+        $HFILES\
+        ${OFILES:%.$O=%.c}\
+
+<$PLAN9/src/mkone
+CFLAGS=$CFLAGS -I../common
diff --git a/src/cmd/upas/vf/vf.c b/src/cmd/upas/vf/vf.c
t@@ -0,0 +1,1110 @@
+/*
+ *  this is a filter that changes mime types and names of
+ *  suspect executable attachments.
+ */
+#include "common.h"
+#include 
+
+Biobuf in;
+Biobuf out;
+
+typedef struct Mtype Mtype;
+typedef struct Hdef Hdef;
+typedef struct Hline Hline;
+typedef struct Part Part;
+
+static int        badfile(char *name);
+static int        badtype(char *type);
+static void        ctype(Part*, Hdef*, char*);
+static void        cencoding(Part*, Hdef*, char*);
+static void        cdisposition(Part*, Hdef*, char*);
+static int        decquoted(char *out, char *in, char *e);
+static char*        getstring(char *p, String *s, int dolower);
+static void        init_hdefs(void);
+static int        isattribute(char **pp, char *attr);
+static int        latin1toutf(char *out, char *in, char *e);
+static String*        mkboundary(void);
+static Part*        part(Part *pp);
+static Part*        passbody(Part *p, int dobound);
+static void        passnotheader(void);
+static void        passunixheader(void);
+static Part*        problemchild(Part *p);
+static void        readheader(Part *p);
+static Hline*        readhl(void);
+static void        readmtypes(void);
+static int        save(Part *p, char *file);
+static void        setfilename(Part *p, char *name);
+static char*        skiptosemi(char *p);
+static char*        skipwhite(char *p);
+static String*        tokenconvert(String *t);
+static void        writeheader(Part *p, int);
+
+enum
+{
+        // encodings
+        Enone=        0,
+        Ebase64,
+        Equoted,
+
+        // disposition possibilities
+        Dnone=        0,
+        Dinline,
+        Dfile,
+        Dignore,
+
+        PAD64=        '=',
+};
+
+/*
+ *  a message part; either the whole message or a subpart
+ */
+struct Part
+{
+        Part        *pp;                /* parent part */
+        Hline        *hl;                /* linked list of header lines */
+        int        disposition;
+        int        encoding;
+        int        badfile;
+        int        badtype;
+        String        *boundary;        /* boundary for multiparts */
+        int        blen;
+        String        *charset;        /* character set */
+        String        *type;                /* content type */
+        String        *filename;        /* file name */
+        Biobuf        *tmpbuf;                /* diversion input buffer */
+};
+
+/*
+ *  a (multi)line header
+ */
+struct Hline
+{
+        Hline        *next;
+        String                *s;
+};
+
+/*
+ *  header definitions for parsing
+ */
+struct Hdef
+{
+        char *type;
+        void (*f)(Part*, Hdef*, char*);
+        int len;
+};
+
+Hdef hdefs[] =
+{
+        { "content-type:", ctype, },
+        { "content-transfer-encoding:", cencoding, },
+        { "content-disposition:", cdisposition, },
+        { 0, },
+};
+
+/*
+ *  acceptable content types and their extensions
+ */
+struct Mtype {
+        Mtype        *next;
+        char         *ext;                /* extension */
+        char        *gtype;                /* generic content type */
+        char        *stype;                /* specific content type */
+        char        class;
+};
+Mtype *mtypes;
+
+int justreject;
+char *savefile;
+
+void
+main(int argc, char **argv)
+{
+        ARGBEGIN{
+        case 'r':
+                justreject = 1;
+                break;
+        case 's':
+                savefile = ARGF();
+                if(savefile == nil)
+                        exits("usage");
+                break;
+        }ARGEND;
+
+        Binit(&in, 0, OREAD);
+        Binit(&out, 1, OWRITE);
+
+        init_hdefs();
+        readmtypes();
+
+        /* pass through our standard 'From ' line */
+        passunixheader();
+
+        /* parse with the top level part */
+        part(nil);
+
+        exits(0);
+}
+
+void
+refuse(void)
+{
+        postnote(PNGROUP, getpid(), "mail refused: we don't accept executable attachments");
+        exits("mail refused: we don't accept executable attachments");
+}
+
+
+/*
+ *  parse a part; returns the ancestor whose boundary terminated
+ *  this part or nil on EOF.
+ */
+static Part*
+part(Part *pp)
+{
+        Part *p, *np;
+
+        p = mallocz(sizeof *p, 1);
+        p->pp = pp;
+        readheader(p);
+
+        if(p->boundary != nil){
+                /* the format of a multipart part is always:
+                 *   header
+                 *   null or ignored body
+                 *   boundary
+                 *   header
+                 *   body
+                 *   boundary
+                 *   ...
+                 */
+                writeheader(p, 1);
+                np = passbody(p, 1);
+                if(np != p)
+                        return np;
+                for(;;){
+                        np = part(p);
+                        if(np != p)
+                                return np;
+                }
+        } else {
+                /* no boundary */
+                /* may still be multipart if this is a forwarded message */
+                if(p->type && cistrcmp(s_to_c(p->type), "message/rfc822") == 0){
+                        /* the format of forwarded message is:
+                         *   header
+                         *   header
+                         *   body
+                         */
+                        writeheader(p, 1);
+                        passnotheader();
+                        return part(p);
+                } else {
+                        /* 
+                         * This is the meat.  This may be an executable.
+                         * if so, wrap it and change its type
+                         */
+                        if(p->badtype || p->badfile){
+                                if(p->badfile == 2){
+                                        if(savefile != nil)
+                                                save(p, savefile);
+                                        syslog(0, "vf", "vf rejected %s %s", p->type?s_to_c(p->type):"?",
+                                                p->filename?s_to_c(p->filename):"?");
+                                        fprint(2, "The mail contained an executable attachment.\n");
+                                        fprint(2, "We refuse all mail containing such.\n");
+                                        refuse();
+                                }
+                                np = problemchild(p);
+                                if(np != p)
+                                        return np;
+                                /* if problemchild returns p, it turns out p is okay: fall thru */
+                        }
+                        writeheader(p, 1);
+                        return passbody(p, 1);
+                }
+        }
+}
+
+/*
+ *  read and parse a complete header
+ */
+static void
+readheader(Part *p)
+{
+        Hline *hl, **l;
+        Hdef *hd;
+
+        l = &p->hl;
+        for(;;){
+                hl = readhl();
+                if(hl == nil)
+                        break;
+                *l = hl;
+                l = &hl->next;
+
+                for(hd = hdefs; hd->type != nil; hd++){
+                        if(cistrncmp(s_to_c(hl->s), hd->type, hd->len) == 0){
+                                (*hd->f)(p, hd, s_to_c(hl->s));
+                                break;
+                        }
+                }
+        }
+}
+
+/*
+ *  read a possibly multiline header line
+ */
+static Hline*
+readhl(void)
+{
+        Hline *hl;
+        String *s;
+        char *p;
+        int n;
+
+        p = Brdline(&in, '\n');
+        if(p == nil)
+                return nil;
+        n = Blinelen(&in);
+        if(memchr(p, ':', n) == nil){
+                Bseek(&in, -n, 1);
+                return nil;
+        }
+        s = s_nappend(s_new(), p, n);
+        for(;;){
+                p = Brdline(&in, '\n');
+                if(p == nil)
+                        break;
+                n = Blinelen(&in);
+                if(*p != ' ' && *p != '\t'){
+                        Bseek(&in, -n, 1);
+                        break;
+                }
+                s = s_nappend(s, p, n);
+        }
+        hl = malloc(sizeof *hl);
+        hl->s = s;
+        hl->next = nil;
+        return hl;
+}
+
+/*
+ *  write out a complete header
+ */
+static void
+writeheader(Part *p, int xfree)
+{
+        Hline *hl, *next;
+
+        for(hl = p->hl; hl != nil; hl = next){
+                Bprint(&out, "%s", s_to_c(hl->s));
+                if(xfree)
+                        s_free(hl->s);
+                next = hl->next;
+                if(xfree)
+                        free(hl);
+        }
+        if(xfree)
+                p->hl = nil;
+}
+
+/*
+ *  pass a body through.  return if we hit one of our ancestors'
+ *  boundaries or EOF.  if we hit a boundary, return a pointer to
+ *  that ancestor.  if we hit EOF, return nil.
+ */
+static Part*
+passbody(Part *p, int dobound)
+{
+        Part *pp;
+        Biobuf *b;
+        char *cp;
+
+        for(;;){
+                if(p->tmpbuf){
+                        b = p->tmpbuf;
+                        cp = Brdline(b, '\n');
+                        if(cp == nil){
+                                Bterm(b);
+                                p->tmpbuf = nil;
+                                goto Stdin;
+                        }
+                }else{
+                Stdin:
+                        b = ∈
+                        cp = Brdline(b, '\n');
+                }
+                if(cp == nil)
+                        return nil;
+                for(pp = p; pp != nil; pp = pp->pp)
+                        if(pp->boundary != nil
+                        && strncmp(cp, s_to_c(pp->boundary), pp->blen) == 0){
+                                if(dobound)
+                                        Bwrite(&out, cp, Blinelen(b));
+                                else
+                                        Bseek(b, -Blinelen(b), 1);
+                                return pp;
+                        }
+                Bwrite(&out, cp, Blinelen(b));
+        }
+        return nil;
+}
+
+/*
+ *  save the message somewhere
+ */
+static vlong bodyoff;        /* clumsy hack */
+static int
+save(Part *p, char *file)
+{
+        int fd;
+        char *cp;
+
+        Bterm(&out);
+        memset(&out, 0, sizeof(out));
+
+        fd = open(file, OWRITE);
+        if(fd < 0)
+                return -1;
+        seek(fd, 0, 2);
+        Binit(&out, fd, OWRITE);
+        cp = ctime(time(0));
+        cp[28] = 0;
+        Bprint(&out, "From virusfilter %s\n", cp);
+        writeheader(p, 0);
+        bodyoff = Boffset(&out);
+        passbody(p, 1);
+        Bprint(&out, "\n");
+        Bterm(&out);
+        close(fd);
+        
+        memset(&out, 0, sizeof out);
+        Binit(&out, 1, OWRITE);
+        return 0;
+}
+
+/*
+ * write to a file but save the fd for passbody.
+ */
+static char*
+savetmp(Part *p)
+{
+        char buf[40], *name;
+        int fd;
+        
+        strcpy(buf, "/tmp/vf.XXXXXXXXXXX");
+        name = mktemp(buf);
+        if((fd = create(name, OWRITE|OEXCL, 0666)) < 0){
+                fprint(2, "error creating temporary file: %r\n");
+                refuse();
+        }
+        close(fd);
+        if(save(p, name) < 0){
+                fprint(2, "error saving temporary file: %r\n");
+                refuse();
+        }
+        if(p->tmpbuf){
+                fprint(2, "error in savetmp: already have tmp file!\n");
+                refuse();
+        }
+        p->tmpbuf = Bopen(name, OREAD|ORCLOSE);
+        if(p->tmpbuf == nil){
+                fprint(2, "error reading tempoary file: %r\n");
+                refuse();
+        }
+        Bseek(p->tmpbuf, bodyoff, 0);
+        return strdup(name);
+}
+
+/*
+ * XXX save the decoded file, run 9 unzip -tf on it, and then
+ * look at the file list.
+ */
+static int
+runchecker(Part *p)
+{
+        int pid;
+        char *name;
+        Waitmsg *w;
+        
+        if(access("/mail/lib/validateattachment", AEXEC) < 0)
+                return 0;
+        
+        name = savetmp(p);
+        fprint(2, "run checker %s\n", name);
+        switch(pid = fork()){
+        case -1:
+                sysfatal("fork: %r");
+        case 0:
+                dup(2, 1);
+                execl("/mail/lib/validateattachment", "validateattachment", name, nil);
+                _exits("exec failed");
+        }
+
+        /*
+         * Okay to return on error - will let mail through but wrapped.
+         */
+        w = wait();
+        if(w == nil){
+                syslog(0, "mail", "vf wait failed: %r");
+                return 0;
+        }
+        if(w->pid != pid){
+                syslog(0, "mail", "vf wrong pid %d != %d", w->pid, pid);
+                return 0;
+        }
+        if(p->filename)
+                name = s_to_c(p->filename);
+        if(strstr(w->msg, "discard")){
+                syslog(0, "mail", "vf validateattachment rejected %s", name);
+                refuse();
+        }
+        if(strstr(w->msg, "accept")){
+                syslog(0, "mail", "vf validateattachment accepted %s", name);
+                return 1;
+        }
+        free(w);
+        return 0;
+}
+
+/*
+ *  emit a multipart Part that explains the problem
+ */
+static Part*
+problemchild(Part *p)
+{
+        Part *np;
+        Hline *hl;
+        String *boundary;
+        char *cp;
+
+        /*
+         * We don't know whether the attachment is okay.
+         * If there's an external checker, let it have a crack at it.
+         */
+        if(runchecker(p) > 0)
+                return p;
+
+fprint(2, "x\n");
+        syslog(0, "mail", "vf wrapped %s %s", p->type?s_to_c(p->type):"?",
+                p->filename?s_to_c(p->filename):"?");
+fprint(2, "x\n");
+
+        boundary = mkboundary();
+fprint(2, "x\n");
+        /* print out non-mime headers */
+        for(hl = p->hl; hl != nil; hl = hl->next)
+                if(cistrncmp(s_to_c(hl->s), "content-", 8) != 0)
+                        Bprint(&out, "%s", s_to_c(hl->s));
+
+fprint(2, "x\n");
+        /* add in our own multipart headers and message */
+        Bprint(&out, "Content-Type: multipart/mixed;\n");
+        Bprint(&out, "\tboundary=\"%s\"\n", s_to_c(boundary));
+        Bprint(&out, "Content-Disposition: inline\n");
+        Bprint(&out, "\n");
+        Bprint(&out, "This is a multi-part message in MIME format.\n");
+        Bprint(&out, "--%s\n", s_to_c(boundary));
+        Bprint(&out, "Content-Disposition: inline\n");
+        Bprint(&out, "Content-Type: text/plain; charset=\"US-ASCII\"\n");
+        Bprint(&out, "Content-Transfer-Encoding: 7bit\n");
+        Bprint(&out, "\n");
+        Bprint(&out, "from postmaster@%s:\n", sysname());
+        Bprint(&out, "The following attachment had content that we can't\n");
+        Bprint(&out, "prove to be harmless.  To avoid possible automatic\n");
+        Bprint(&out, "execution, we changed the content headers.\n");
+        Bprint(&out, "The original header was:\n\n");
+
+        /* print out original header lines */
+        for(hl = p->hl; hl != nil; hl = hl->next)
+                if(cistrncmp(s_to_c(hl->s), "content-", 8) == 0)
+                        Bprint(&out, "\t%s", s_to_c(hl->s));
+        Bprint(&out, "--%s\n", s_to_c(boundary));
+
+        /* change file name */
+        if(p->filename)
+                s_append(p->filename, ".suspect");
+        else
+                p->filename = s_copy("file.suspect");
+
+        /* print out new header */
+        Bprint(&out, "Content-Type: application/octet-stream\n");
+        Bprint(&out, "Content-Disposition: attachment; filename=\"%s\"\n", s_to_c(p->filename));
+        switch(p->encoding){
+        case Enone:
+                break;
+        case Ebase64:
+                Bprint(&out, "Content-Transfer-Encoding: base64\n");
+                break;
+        case Equoted:
+                Bprint(&out, "Content-Transfer-Encoding: quoted-printable\n");
+                break;
+        }
+
+fprint(2, "z\n");
+        /* pass the body */
+        np = passbody(p, 0);
+
+fprint(2, "w\n");
+        /* add the new boundary and the original terminator */
+        Bprint(&out, "--%s--\n", s_to_c(boundary));
+        if(np && np->boundary){
+                cp = Brdline(&in, '\n');
+                Bwrite(&out, cp, Blinelen(&in));
+        }
+
+fprint(2, "a %p\n", np);
+        return np;
+}
+
+static int
+isattribute(char **pp, char *attr)
+{
+        char *p;
+        int n;
+
+        n = strlen(attr);
+        p = *pp;
+        if(cistrncmp(p, attr, n) != 0)
+                return 0;
+        p += n;
+        while(*p == ' ')
+                p++;
+        if(*p++ != '=')
+                return 0;
+        while(*p == ' ')
+                p++;
+        *pp = p;
+        return 1;
+}
+
+/*
+ *  parse content type header 
+ */
+static void
+ctype(Part *p, Hdef *h, char *cp)
+{
+        String *s;
+
+        cp += h->len;
+        cp = skipwhite(cp);
+
+        p->type = s_new();
+        cp = getstring(cp, p->type, 1);
+        if(badtype(s_to_c(p->type)))
+                p->badtype = 1;
+        
+        while(*cp){
+                if(isattribute(&cp, "boundary")){
+                        s = s_new();
+                        cp = getstring(cp, s, 0);
+                        p->boundary = s_reset(p->boundary);
+                        s_append(p->boundary, "--");
+                        s_append(p->boundary, s_to_c(s));
+                        p->blen = s_len(p->boundary);
+                        s_free(s);
+                } else if(cistrncmp(cp, "multipart", 9) == 0){
+                        /*
+                         *  the first unbounded part of a multipart message,
+                         *  the preamble, is not displayed or saved
+                         */
+                } else if(isattribute(&cp, "name")){
+                        setfilename(p, cp);
+                } else if(isattribute(&cp, "charset")){
+                        if(p->charset == nil)
+                                p->charset = s_new();
+                        cp = getstring(cp, s_reset(p->charset), 0);
+                }
+                
+                cp = skiptosemi(cp);
+        }
+}
+
+/*
+ *  parse content encoding header 
+ */
+static void
+cencoding(Part *m, Hdef *h, char *p)
+{
+        p += h->len;
+        p = skipwhite(p);
+        if(cistrncmp(p, "base64", 6) == 0)
+                m->encoding = Ebase64;
+        else if(cistrncmp(p, "quoted-printable", 16) == 0)
+                m->encoding = Equoted;
+}
+
+/*
+ *  parse content disposition header 
+ */
+static void
+cdisposition(Part *p, Hdef *h, char *cp)
+{
+        cp += h->len;
+        cp = skipwhite(cp);
+        while(*cp){
+                if(cistrncmp(cp, "inline", 6) == 0){
+                        p->disposition = Dinline;
+                } else if(cistrncmp(cp, "attachment", 10) == 0){
+                        p->disposition = Dfile;
+                } else if(cistrncmp(cp, "filename=", 9) == 0){
+                        cp += 9;
+                        setfilename(p, cp);
+                }
+                cp = skiptosemi(cp);
+        }
+
+}
+
+static void
+setfilename(Part *p, char *name)
+{
+        if(p->filename == nil)
+                p->filename = s_new();
+        getstring(name, s_reset(p->filename), 0);
+        p->filename = tokenconvert(p->filename);
+        p->badfile = badfile(s_to_c(p->filename));
+}
+
+static char*
+skipwhite(char *p)
+{
+        while(isspace(*p))
+                p++;
+        return p;
+}
+
+static char*
+skiptosemi(char *p)
+{
+        while(*p && *p != ';')
+                p++;
+        while(*p == ';' || isspace(*p))
+                p++;
+        return p;
+}
+
+/*
+ *  parse a possibly "'d string from a header.  A
+ *  ';' terminates the string.
+ */
+static char*
+getstring(char *p, String *s, int dolower)
+{
+        s = s_reset(s);
+        p = skipwhite(p);
+        if(*p == '"'){
+                p++;
+                for(;*p && *p != '"'; p++)
+                        if(dolower)
+                                s_putc(s, tolower(*p));
+                        else
+                                s_putc(s, *p);
+                if(*p == '"')
+                        p++;
+                s_terminate(s);
+
+                return p;
+        }
+
+        for(; *p && !isspace(*p) && *p != ';'; p++)
+                if(dolower)
+                        s_putc(s, tolower(*p));
+                else
+                        s_putc(s, *p);
+        s_terminate(s);
+
+        return p;
+}
+
+static void
+init_hdefs(void)
+{
+        Hdef *hd;
+        static int already;
+
+        if(already)
+                return;
+        already = 1;
+
+        for(hd = hdefs; hd->type != nil; hd++)
+                hd->len = strlen(hd->type);
+}
+
+/*
+ *  create a new boundary
+ */
+static String*
+mkboundary(void)
+{
+        char buf[32];
+        int i;
+        static int already;
+
+        if(already == 0){
+                srand((time(0)<<16)|getpid());
+                already = 1;
+        }
+        strcpy(buf, "upas-");
+        for(i = 5; i < sizeof(buf)-1; i++)
+                buf[i] = 'a' + nrand(26);
+        buf[i] = 0;
+        return s_copy(buf);
+}
+
+/*
+ *  skip blank lines till header
+ */
+static void
+passnotheader(void)
+{
+        char *cp;
+        int i, n;
+
+        while((cp = Brdline(&in, '\n')) != nil){
+                n = Blinelen(&in);
+                for(i = 0; i < n-1; i++)
+                        if(cp[i] != ' ' && cp[i] != '\t' && cp[i] != '\r'){
+                                Bseek(&in, -n, 1);
+                                return;
+                        }
+                Bwrite(&out, cp, n);
+        }
+}
+
+/*
+ *  pass unix header lines
+ */
+static void
+passunixheader(void)
+{
+        char *p;
+        int n;
+
+        while((p = Brdline(&in, '\n')) != nil){
+                n = Blinelen(&in);
+                if(strncmp(p, "From ", 5) != 0){
+                        Bseek(&in, -n, 1);
+                        break;
+                }
+                Bwrite(&out, p, n);
+        }
+}
+
+/*
+ *  Read mime types
+ */
+static void
+readmtypes(void)
+{
+        Biobuf *b;
+        char *p;
+        char *f[6];
+        Mtype *m;
+        Mtype **l;
+
+        b = Bopen(unsharp("#9/sys/lib/mimetype"), OREAD);
+        if(b == nil)
+                return;
+
+        l = &mtypes;
+        while((p = Brdline(b, '\n')) != nil){
+                if(*p == '#')
+                        continue;
+                p[Blinelen(b)-1] = 0;
+                if(tokenize(p, f, nelem(f)) < 5)
+                        continue;
+                m = mallocz(sizeof *m, 1);
+                if(m == nil)
+                        goto err;
+                m->ext = strdup(f[0]);
+                if(m->ext == 0)
+                        goto err;
+                m->gtype = strdup(f[1]);
+                if(m->gtype == 0)
+                        goto err;
+                m->stype = strdup(f[2]);
+                if(m->stype == 0)
+                        goto err;
+                m->class = *f[4];
+                *l = m;
+                l = &(m->next);
+        }
+        Bterm(b);
+        return;
+err:
+        if(m == nil)
+                return;
+        free(m->ext);
+        free(m->gtype);
+        free(m->stype);
+        free(m);
+        Bterm(b);
+}
+
+/*
+ *  if the class is 'm' or 'y', accept it
+ *  if the class is 'p' check a previous extension
+ *  otherwise, filename is bad
+ */
+static int
+badfile(char *name)
+{
+        char *p;
+        Mtype *m;
+        int rv;
+
+        p = strrchr(name, '.');
+        if(p == nil)
+                return 0;
+
+        for(m = mtypes; m != nil; m = m->next)
+                if(cistrcmp(p, m->ext) == 0){
+                        switch(m->class){
+                        case 'm':
+                        case 'y':
+                                return 0;
+                        case 'p':
+                                *p = 0;
+                                rv = badfile(name);
+                                *p = '.';
+                                return rv;
+                        case 'r':
+                                return 2;
+                        }
+                }
+        if(justreject)
+                return 0;
+        return 1;
+}
+
+/*
+ *  if the class is 'm' or 'y' or 'p', accept it
+ *  otherwise, filename is bad
+ */
+static int
+badtype(char *type)
+{
+        Mtype *m;
+        char *s, *fix;
+        int rv = 1;
+
+        if(justreject)
+                return 0;
+
+        fix = s = strchr(type, '/');
+        if(s != nil)
+                *s++ = 0;
+        else
+                s = "-";
+
+        for(m = mtypes; m != nil; m = m->next){
+                if(cistrcmp(type, m->gtype) != 0)
+                        continue;
+                if(cistrcmp(s, m->stype) != 0)
+                        continue;
+                switch(m->class){
+                case 'y':
+                case 'p':
+                case 'm':
+                        rv = 0;
+                        break;
+                }
+                break;
+        }
+
+        if(fix != nil)
+                *fix = '/';
+        return rv;
+}
+
+/* rfc2047 non-ascii */
+typedef struct Charset Charset;
+struct Charset {
+        char *name;
+        int len;
+        int convert;
+} charsets[] =
+{
+        { "us-ascii",                8,        1, },
+        { "utf-8",                5,        0, },
+        { "iso-8859-1",                10,        1, },
+};
+
+/*
+ *  convert to UTF if need be
+ */
+static String*
+tokenconvert(String *t)
+{
+        String *s;
+        char decoded[1024];
+        char utfbuf[2*1024];
+        int i, len;
+        char *e;
+        char *token;
+
+        token = s_to_c(t);
+        len = s_len(t);
+
+        if(token[0] != '=' || token[1] != '?' ||
+           token[len-2] != '?' || token[len-1] != '=')
+                goto err;
+        e = token+len-2;
+        token += 2;
+
+        // bail if we don't understand the character set
+        for(i = 0; i < nelem(charsets); i++)
+                if(cistrncmp(charsets[i].name, token, charsets[i].len) == 0)
+                if(token[charsets[i].len] == '?'){
+                        token += charsets[i].len + 1;
+                        break;
+                }
+        if(i >= nelem(charsets))
+                goto err;
+
+        // bail if it doesn't fit 
+        if(strlen(token) > sizeof(decoded)-1)
+                goto err;
+
+        // bail if we don't understand the encoding
+        if(cistrncmp(token, "b?", 2) == 0){
+                token += 2;
+                len = dec64((uchar*)decoded, sizeof(decoded), token, e-token);
+                decoded[len] = 0;
+        } else if(cistrncmp(token, "q?", 2) == 0){
+                token += 2;
+                len = decquoted(decoded, token, e);
+                if(len > 0 && decoded[len-1] == '\n')
+                        len--;
+                decoded[len] = 0;
+        } else
+                goto err;
+
+        s = nil;
+        switch(charsets[i].convert){
+        case 0:
+                s = s_copy(decoded);
+                break;
+        case 1:
+                s = s_new();
+                latin1toutf(utfbuf, decoded, decoded+len);
+                s_append(s, utfbuf);
+                break;
+        }
+
+        return s;
+err:
+        return s_clone(t);
+}
+
+/*
+ *  decode quoted 
+ */
+enum
+{
+        Self=        1,
+        Hex=        2,
+};
+uchar        tableqp[256];
+
+static void
+initquoted(void)
+{
+        int c;
+
+        memset(tableqp, 0, 256);
+        for(c = ' '; c <= '<'; c++)
+                tableqp[c] = Self;
+        for(c = '>'; c <= '~'; c++)
+                tableqp[c] = Self;
+        tableqp['\t'] = Self;
+        tableqp['='] = Hex;
+}
+
+static int
+hex2int(int x)
+{
+        if(x >= '0' && x <= '9')
+                return x - '0';
+        if(x >= 'A' && x <= 'F')
+                return (x - 'A') + 10;
+        if(x >= 'a' && x <= 'f')
+                return (x - 'a') + 10;
+        return 0;
+}
+
+static char*
+decquotedline(char *out, char *in, char *e)
+{
+        int c, soft;
+
+        /* dump trailing white space */
+        while(e >= in && (*e == ' ' || *e == '\t' || *e == '\r' || *e == '\n'))
+                e--;
+
+        /* trailing '=' means no newline */
+        if(*e == '='){
+                soft = 1;
+                e--;
+        } else
+                soft = 0;
+
+        while(in <= e){
+                c = (*in++) & 0xff;
+                switch(tableqp[c]){
+                case Self:
+                        *out++ = c;
+                        break;
+                case Hex:
+                        c = hex2int(*in++)<<4;
+                        c |= hex2int(*in++);
+                        *out++ = c;
+                        break;
+                }
+        }
+        if(!soft)
+                *out++ = '\n';
+        *out = 0;
+
+        return out;
+}
+
+static int
+decquoted(char *out, char *in, char *e)
+{
+        char *p, *nl;
+
+        if(tableqp[' '] == 0)
+                initquoted();
+
+        p = out;
+        while((nl = strchr(in, '\n')) != nil && nl < e){
+                p = decquotedline(p, in, nl);
+                in = nl + 1;
+        }
+        if(in < e)
+                p = decquotedline(p, in, e-1);
+
+        // make sure we end with a new line
+        if(*(p-1) != '\n'){
+                *p++ = '\n';
+                *p = 0;
+        }
+
+        return p - out;
+}
+
+/* translate latin1 directly since it fits neatly in utf */
+static int
+latin1toutf(char *out, char *in, char *e)
+{
+        Rune r;
+        char *p;
+
+        p = out;
+        for(; in < e; in++){
+                r = (*in) & 0xff;
+                p += runetochar(p, &r);
+        }
+        *p = 0;
+        return p - out;
+}