shithub: fork

Download patch

ref: 4baf1f15bd5dc66b815863eed931a9132a70ee7a
parent: e06e4ff0a58207476cf37424d5b9df2254003c77
author: qwx <[email protected]>
date: Sun Aug 13 23:18:00 EDT 2023

nupas: ignore certs

diff: cannot open b/sys/src/cmd/upas/Mail//null: file does not exist: 'b/sys/src/cmd/upas/Mail//null' diff: cannot open b/sys/src/cmd/upas/alias//null: file does not exist: 'b/sys/src/cmd/upas/alias//null' diff: cannot open b/sys/src/cmd/upas/bayes//null: file does not exist: 'b/sys/src/cmd/upas/bayes//null' diff: cannot open b/sys/src/cmd/upas/binscripts//null: file does not exist: 'b/sys/src/cmd/upas/binscripts//null' diff: cannot open b/sys/src/cmd/upas/common//null: file does not exist: 'b/sys/src/cmd/upas/common//null' diff: cannot open b/sys/src/cmd/upas/dkim//null: file does not exist: 'b/sys/src/cmd/upas/dkim//null' diff: cannot open b/sys/src/cmd/upas/filterkit//null: file does not exist: 'b/sys/src/cmd/upas/filterkit//null' diff: cannot open b/sys/src/cmd/upas/fs/extra//null: file does not exist: 'b/sys/src/cmd/upas/fs/extra//null' diff: cannot open b/sys/src/cmd/upas/fs//null: file does not exist: 'b/sys/src/cmd/upas/fs//null' diff: cannot open b/sys/src/cmd/upas/imap4d//null: file does not exist: 'b/sys/src/cmd/upas/imap4d//null' diff: cannot open b/sys/src/cmd/upas/marshal//null: file does not exist: 'b/sys/src/cmd/upas/marshal//null' diff: cannot open b/sys/src/cmd/upas/ml//null: file does not exist: 'b/sys/src/cmd/upas/ml//null' diff: cannot open b/sys/src/cmd/upas/ned//null: file does not exist: 'b/sys/src/cmd/upas/ned//null' diff: cannot open b/sys/src/cmd/upas/pop3//null: file does not exist: 'b/sys/src/cmd/upas/pop3//null' diff: cannot open b/sys/src/cmd/upas/q//null: file does not exist: 'b/sys/src/cmd/upas/q//null' diff: cannot open b/sys/src/cmd/upas/qfrom//null: file does not exist: 'b/sys/src/cmd/upas/qfrom//null' diff: cannot open b/sys/src/cmd/upas/scanmail//null: file does not exist: 'b/sys/src/cmd/upas/scanmail//null' diff: cannot open b/sys/src/cmd/upas/send//null: file does not exist: 'b/sys/src/cmd/upas/send//null' diff: cannot open b/sys/src/cmd/upas/smtp//null: file does not exist: 'b/sys/src/cmd/upas/smtp//null' diff: cannot open b/sys/src/cmd/upas/spf//null: file does not exist: 'b/sys/src/cmd/upas/spf//null' diff: cannot open b/sys/src/cmd/upas/unesc//null: file does not exist: 'b/sys/src/cmd/upas/unesc//null' diff: cannot open b/sys/src/cmd/upas/vf//null: file does not exist: 'b/sys/src/cmd/upas/vf//null' diff: cannot open b/sys/src/cmd/upas//null: file does not exist: 'b/sys/src/cmd/upas//null' diff: cannot open b/sys/src/cmd//null: file does not exist: 'b/sys/src/cmd//null'
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/comp.c
@@ -1,0 +1,286 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+typedef struct Fn	Fn;
+
+struct Fn {
+	char *name;
+	void (*fn)(Comp *, char **, int);
+};
+
+void
+execmarshal(void *p)
+{
+	Comp *c;
+	char *av[8];
+	int na;
+
+	c = p;
+	rfork(RFFDG);
+	dup(c->fd[0], 0);
+	close(c->fd[0]);
+	close(c->fd[1]);
+
+	na = 0;
+	av[na++] = "marshal";
+	av[na++] = "-8";
+	if(savebox != nil){
+		av[na++] = "-S";
+		av[na++] = savebox;
+	}
+	if(c->rpath != nil){
+		av[na++] = "-R";
+		av[na++] = c->rpath;
+	}
+	av[na] = nil;
+	assert(na < nelem(av));
+	procexec(c->sync, "/bin/upas/marshal", av);
+}
+
+static void
+postmesg(Comp *c, char **, int nf)
+{
+	char *buf, wpath[64], *path;
+	int n, fd;
+	Mesg *m;
+
+	snprint(wpath, sizeof(wpath), "/mnt/acme/%d/body", c->id);
+	if(nf != 0){
+		fprint(2, "Post: too many args\n");
+		return;
+	}
+	if((fd = open(wpath, OREAD)) == -1){
+		fprint(2, "open body: %r\n");
+		return;
+	}
+	if(pipe(c->fd) == -1)
+		sysfatal("pipe: %r\n");
+
+	c->sync = chancreate(sizeof(ulong), 0);
+	procrfork(execmarshal, c, Stack, RFNOTEG);
+	recvul(c->sync);
+	chanfree(c->sync);
+	close(c->fd[0]);
+
+	buf = emalloc(Bufsz);
+	while((n = read(fd, buf, Bufsz)) > 0)
+		if(write(c->fd[1], buf, n) != n)
+			break;
+	write(c->fd[1], "\n", 1);
+	close(c->fd[1]);
+	close(fd);
+	if(n == -1)
+		return;
+
+	if(fprint(c->ctl, "name %s:Sent\n", c->path) == -1)
+		sysfatal("write ctl: %r");
+	if(c->replyto != nil){
+		if((m = mesglookup(c->rname, c->rdigest)) == nil)
+			return;
+		m->flags |= Fresp;
+		path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
+		if((fd = open(path, OWRITE)) != -1){
+			fprint(fd, "+a");
+			close(fd);
+		}
+		mbredraw(m, 0, 0);
+		free(path);
+	}
+	fprint(c->ctl, "clean\n");
+}
+
+static void
+compquit(Comp *c, char **, int)
+{
+	c->quitting = 1;
+}
+
+static Fn compfn[] = {
+	{"Post", postmesg},
+	{"Del", compquit},
+	{nil},
+};
+
+static void
+compmain(void *cp)
+{
+	char *f[32];
+	int nf;
+	Event ev;
+	Comp *c, **pc;
+	Fn *p;
+
+	c = cp;
+	c->quitting = 0;
+	c->qnext = mbox.opencomp;
+	mbox.opencomp = c;
+	fprint(c->ctl, "clean\n");
+	mbox.nopen++;
+	while(!c->quitting){
+		if(winevent(c, &ev) != 'M')
+			continue;
+		if(strcmp(ev.text, "Del") == 0)
+			break;
+		switch(ev.type){
+		case 'l':
+		case 'L':
+			if(matchmesg(&mbox, ev.text))
+				mesgopen(ev.text, nil);
+			else
+				winreturn(c, &ev);
+			break;
+		case 'x':
+		case 'X':
+			if((nf = tokenize(ev.text, f, nelem(f))) == 0)
+				continue;
+			for(p = compfn; p->fn != nil; p++)
+				if(strcmp(p->name, f[0]) == 0){
+					p->fn(c, &f[1], nf - 1);
+					break;
+				}
+			if(p->fn == nil)
+				winreturn(c, &ev);
+			break;
+		break;
+		}
+	}
+	for(pc = &mbox.opencomp; *pc != nil; pc = &(*pc)->qnext)
+		if(*pc == c){
+			*pc = c->qnext;
+			break;
+		}
+	mbox.nopen--;
+	c->qnext = nil;
+	winclose(c);
+	free(c->replyto);
+	free(c->rname);
+	free(c->rdigest);
+	free(c->rpath);
+	threadexits(nil);
+}
+
+static Biobuf*
+openbody(Mesg *r)
+{
+	Biobuf *f;
+	int q0, q1;
+	char *s;
+
+	assert(r->state & Sopen);
+
+	wingetsel(r, &q0, &q1);
+	if(q1 - q0 != 0){
+		s = smprint("/mnt/acme/%d/xdata", r->id);
+		f = Bopen(s, OREAD);
+		free(s);
+	}else
+		f = mesgopenbody(r);
+	return f;
+}
+
+int
+strpcmp(void *a, void *b)
+{
+	return strcmp(*(char**)a, *(char**)b);
+}
+
+void
+show(Biobuf *fd, char *type, char **addrs, int naddrs)
+{
+	char *sep;
+	int i, w;
+
+	w = 0;
+	sep = "";
+	if(naddrs == 0)
+		return;
+	qsort(addrs, naddrs, sizeof(char*), strpcmp);
+	for(i = 1; i < naddrs; i++){
+		if(strcmp(addrs[i-1], addrs[i]) == 0)
+			addrs[i-1] = nil;
+	}
+	Bprint(fd, "%s: ", type);
+	for(i = 0; i < naddrs; i++){
+		if(addrs[i] == nil)
+			continue;
+		w += Bprint(fd, "%s%s", sep, addrs[i]);
+		sep = ", ";
+	}
+	Bprint(fd, "\n");
+}
+
+void
+respondto(Biobuf *fd, char *to, Mesg *r, int all)
+{
+	char *rpto, **addrs;
+	int n;
+
+	rpto = to;
+	if(r != nil)
+		rpto = (strlen(r->replyto) > 0) ? r->replyto : r->from;
+	if(r == nil || !all){
+		Bprint(fd, "To: %s\n", rpto);
+		return;
+	}
+
+	addrs = emalloc(64*sizeof(char*));
+	n = 0;
+	n += tokenize(to, addrs+n, 64-n);
+	n += tokenize(rpto, addrs+n, 64-n);
+	n += tokenize(r->to, addrs+n, 64-n);
+	show(fd, "To", addrs, n);
+	n = 0;
+	n += tokenize(r->cc, addrs+n, 64-n);
+	show(fd, "CC", addrs, n);
+	free(addrs);
+}
+
+void
+compose(char *to, Mesg *r, int all)
+{
+	static int ncompose;
+	Biobuf *rfd, *wfd;
+	Comp *c;
+	char *ln;
+
+	c = emalloc(sizeof(Comp));
+	if(r != nil)
+		c->path = esmprint("%s%s%s.%d", mbox.path, r->name, "Reply", ncompose++);
+	else
+		c->path = esmprint("%sCompose.%d", mbox.path, ncompose++);
+	wininit(c, c->path);
+
+	wintagwrite(c, "Post |fmt ");
+	wfd = bwinopen(c, "body", OWRITE);
+	respondto(wfd, to, r, all);
+	if(r == nil)
+		Bprint(wfd, "Subject: ");
+	else{
+		if(r->messageid != nil)
+			c->replyto = estrdup(r->messageid);
+		c->rpath = estrjoin(mbox.path, r->name, nil);
+		c->rname = estrdup(r->name);
+		c->rdigest = estrdup(r->digest);
+		Bprint(wfd, "Subject: ");
+		if(r->subject != nil && cistrncmp(r->subject, "Re", 2) != 0)
+			Bprint(wfd, "Re: ");
+		Bprint(wfd, "%s\n\n", r->subject);
+		Bprint(wfd, "Quoth %s:\n", r->fromcolon);
+		rfd = openbody(r);
+		if(rfd != nil){
+			while((ln = Brdstr(rfd, '\n', 0)) != nil)
+				if(Bprint(wfd, "> %s", ln) == -1)
+					break;
+			Bterm(rfd);
+		}
+	}
+	Bterm(wfd);
+	fprint(c->addr, "$");
+	fprint(c->ctl, "dot=addr");
+	threadcreate(compmain, c, Stack);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/mail.h
@@ -1,0 +1,196 @@
+typedef struct Event	Event;
+typedef struct Win	Win;
+typedef struct Mesg	Mesg;
+typedef struct Mbox	Mbox;
+typedef struct Comp	Comp;
+
+enum {
+	Stack	= 64*1024,
+	Bufsz	= 8192,
+	Eventsz	= 256*UTFmax,
+	Subjlen	= 56,
+};
+
+enum {
+	Sdummy	= 1<<0,	/* message placeholder */
+	Stoplev	= 1<<1,	/* not a response to anything */
+	Sopen	= 1<<2,	/* opened for viewing */
+	Szap	= 1<<3, /* flushed, to be removed from list */
+};
+
+enum {
+	Fresp	= 1<<0,	/* has been responded to */
+	Fseen	= 1<<1,	/* has been viewed */
+	Fdel	= 1<<2, /* was deleted */
+	Ftodel	= 1<<3,	/* pending deletion */
+};
+
+enum {
+	Vflat,
+	Vgroup,
+};
+
+struct Event {
+	char	action;
+	char	type;
+	int	p0;	/* click point */
+	int	q0;	/* expand lo */
+	int	q1;	/* expand hi */
+	int	flags;
+	int	ntext;
+	char	text[Eventsz + 1];
+};
+
+struct Win {
+	int	id;
+	Ioproc	*io;
+	Biobuf	*event;
+	int	revent;
+	int	ctl;
+	int	addr;
+	int	data;
+	int	open;
+};
+
+/*
+ * In progress compositon
+ */
+struct Comp {
+	Win;
+
+	/* exec setup */
+	Channel *sync;
+	int	fd[2];
+
+	/* to relate back the message */
+	char	*replyto;
+	char	*rname;
+	char	*rpath;
+	char	*rdigest;
+	char	*path;
+
+	int	quitting;
+	Comp	*qnext;
+};
+
+/*
+ * Message in mailbox
+ */
+struct Mesg {
+	Win;
+
+	/* bookkeeping */
+	char	*name;
+	int	state;
+	int	flags;
+	u32int	hash;
+	char	quitting;
+	Mesg	*qnext;
+
+	/* exec setup */
+	Channel *sync;
+	char	*path;
+	int	fd[2];
+
+	Mesg	*parent;
+	Mesg	**child;
+	int	nchild;
+	int	nsub;	/* transitive children */
+	
+	Mesg	*body;	/* best attachment to use, or nil */
+	Mesg	**parts;
+	int	nparts;
+	int xparts;
+
+	/* info fields */
+	char	*from;
+	char	*to;
+	char	*cc;
+	char	*replyto;
+	char	*date;
+	char	*subject;
+	char	*type;
+	char	*disposition;
+	char	*messageid;
+	char	*filename;
+	char	*digest;
+	char	*mflags;
+	char	*fromcolon;
+	char	*inreplyto;
+
+	vlong	time;
+};
+
+/*
+ *The mailbox we're showing.
+ */
+struct Mbox {
+	Win;
+
+	Mesg	**mesg;
+	Mesg	**hash;
+	int	mesgsz;
+	int	hashsz;
+	int	nmesg;
+	int	ndead;
+
+	Mesg	*openmesg;
+	Comp	*opencomp;
+	int	canquit;
+
+	Channel	*see;
+	Channel	*show;
+	Channel	*event;
+	Channel	*send;
+
+	int	view;
+	int	nopen;
+	char	*path;
+};
+
+extern Mbox	mbox;
+extern Reprog	*mesgpat;
+extern char	*savebox;
+
+/* window management */
+void	wininit(Win*, char*);
+int	winopen(Win*, char*, int);
+Biobuf	*bwinopen(Win*, char*, int);
+Biobuf	*bwindata(Win*, int);
+void	winclose(Win*);
+void	wintagwrite(Win*, char*);
+int	winevent(Win*, Event*);
+void	winreturn(Win*, Event*);
+int	winread(Win*, int, int, char*, int);
+int	matchmesg(Win*, char*);
+char	*winreadsel(Win*);
+void	wingetsel(Win*, int*, int*);
+void	winsetsel(Win*, int, int);
+
+/* messages */
+Mesg	*mesglookup(char*, char*);
+Mesg	*mesgload(char*);
+Mesg	*mesgopen(char*, char*);
+int	mesgmatch(Mesg*, char*, char*);
+void	mesgclear(Mesg*);
+void	mesgfree(Mesg*);
+void	mesgpath2name(char*, int, char*);
+int	mesgflagparse(char*, int*);
+Biobuf*	mesgopenbody(Mesg*);
+
+/* mailbox */
+void	mbredraw(Mesg*, int, int);
+
+/* composition */
+void	compose(char*, Mesg*, int);
+
+/* utils */
+void	*emalloc(ulong);
+void	*erealloc(void*, ulong);
+char	*estrdup(char*);
+char	*estrjoin(char*, ...);
+char	*esmprint(char*, ...);
+char	*rslurp(Mesg*, char*, int*);
+char	*fslurp(int, int*);
+u32int	strhash(char*);
+
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/mbox.c
@@ -1,0 +1,1062 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <plumb.h>
+#include <ctype.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+typedef struct Fn	Fn;
+
+struct Fn {
+	char *name;
+	void (*fn)(char **, int);
+};
+
+enum {
+	Cevent,
+	Cseemail,
+	Cshowmail,
+	Csendmail,
+	Nchan,
+};
+
+
+char	*maildir	= "/mail/fs";
+char	*mailbox	= "mbox";
+char	*savebox	= "outgoing";
+char	*listfmt	= "%>48s\t<%f>";
+Mesg	dead = {.messageid="", .hash=42};
+
+Reprog	*mesgpat;
+
+int	threadsort = 1;
+int	sender;
+
+int	plumbsendfd;
+int	plumbseemailfd;
+int	plumbshowmailfd;
+int	plumbsendmailfd;
+Channel *cwait;
+
+Mbox	mbox;
+
+static void	showmesg(Biobuf*, Mesg*, int, int);
+
+static void
+plumbloop(Channel *ch, int fd)
+{
+	Plumbmsg *m;
+
+	while(1){
+		if((m = plumbrecv(fd)) == nil)
+			threadexitsall("plumber gone");
+		sendp(ch, m);
+	}
+}
+
+static void
+plumbshowmail(void*)
+{
+	threadsetname("plumbshow %s", mbox.path);
+	plumbloop(mbox.show, plumbshowmailfd);
+}
+
+static void
+plumbseemail(void*)
+{
+	threadsetname("plumbsee %s", mbox.path);
+	plumbloop(mbox.see, plumbseemailfd);
+}
+
+static void
+plumbsendmail(void*)
+{
+	threadsetname("plumbsend %s", mbox.path);
+	plumbloop(mbox.send, plumbsendmailfd);
+}
+
+static void
+eventread(void*)
+{
+	Event *ev;
+
+	threadsetname("mbevent %s", mbox.path);
+	while(1){
+		ev = emalloc(sizeof(Event));
+		if(winevent(&mbox, ev) == -1)
+			break;
+		sendp(mbox.event, ev);
+	}
+	sendp(mbox.event, nil);
+	threadexits(nil);
+}
+
+static int
+ideq(char *a, char *b)
+{
+	if(a == nil || b == nil)
+		return 0;
+	return strcmp(a, b) == 0;
+}
+
+static int
+cmpmesg(void *pa, void *pb)
+{
+	Mesg *a, *b;
+
+	a = *(Mesg**)pa;
+	b = *(Mesg**)pb;
+
+	return b->time - a->time;
+}
+
+static int
+rcmpmesg(void *pa, void *pb)
+{
+	Mesg *a, *b;
+
+	a = *(Mesg**)pa;
+	b = *(Mesg**)pb;
+
+	return a->time - b->time;
+}
+
+static int
+mesglineno(Mesg *msg, int *depth)
+{
+	Mesg *p, *m;
+	int i, o, n, d;
+
+	o = 0;
+	d = 0;
+	n = 1;
+
+	/* Walk up to root, counting depth in the thread */
+	p = msg;
+	while(p->parent != nil){
+		m = p;
+		p = p->parent;
+		for(i = 0; i < p->nchild; i++){
+			if(p->child[i] == m)
+				break;
+			o += p->child[i]->nsub + 1;
+		}
+		if(!(p->state & (Sdummy|Szap))){
+			o++;
+			d++;
+		}
+	}
+
+	/* Find the thread in the thread list */
+	for(i = 0; i < mbox.nmesg; i++){
+		m = mbox.mesg[i];
+		if(m == p)
+			break;
+		if(m->state & Stoplev){
+			n += mbox.mesg[i]->nsub;
+			if(!(m->state & (Sdummy|Szap)))
+				n++;
+		}
+
+	}
+	if(depth != nil)
+		*depth = d;
+	assert(n + o <= mbox.nmesg);
+	return n + o;
+}
+
+static int
+addchild(Mesg *p, Mesg *m, int d)
+{
+	Mesg *q;
+
+	assert(m->parent == nil);
+	for(q = p; q != nil; q = q->parent){
+		/* some messages refer to themselves */
+		if(ideq(m->messageid, q->messageid))
+			return 0;
+		if(m->time > q->time)
+			q->time = m->time;
+	}
+	for(q = p; q != nil; q = q->parent)
+		q->nsub += d;
+	p->child = erealloc(p->child, ++p->nchild*sizeof(Mesg*));
+	p->child[p->nchild - 1] = m;
+	qsort(p->child, p->nchild, sizeof(Mesg*), rcmpmesg);
+	m->parent = p;
+	return 1;
+}
+
+static int
+slotfor(Mesg *m)
+{
+	int i;
+
+	for(i = 0; i < mbox.nmesg; i++)
+		if(cmpmesg(&mbox.mesg[i], &m) >= 0)
+			break;
+	return i;
+}
+
+static void
+removeid(Mesg *m)
+{
+	Mesg *e;
+	int i;
+
+	/* Dummies don't go in the table */
+	if(m->state & Sdummy)
+		return;
+	i = m->hash % mbox.hashsz;
+	while(1){
+		e = mbox.hash[i];
+		if(e == nil)
+			return;
+		if(e != &dead && e->hash == m->hash && strcmp(e->messageid, m->messageid) == 0){
+			mbox.hash[i] = &dead;
+			mbox.ndead++;
+		}
+		i = (i + 1) % mbox.hashsz;
+	}
+}
+
+Mesg*
+lookupid(char *msgid)
+{
+	u32int h, i;
+	Mesg *e;
+
+	if(msgid == nil || strlen(msgid) == 0)
+		return nil;
+	h = strhash(msgid);
+	i = h % mbox.hashsz;
+	while(1){
+		e = mbox.hash[i];
+		if(e == nil)
+			return nil;
+		if(e != &dead && e->hash == h && strcmp(e->messageid, msgid) == 0)
+			return e;
+		i = (i + 1) % mbox.hashsz;
+	}
+}
+
+static void
+addmesg(Mesg *m, int ins)
+{
+	Mesg *o, *e, **oldh;
+	int i, oldsz, idx;
+
+	/* 
+	 * on initial load, it's faster to append everything then sort,
+	 * but on subsequent messages it's better to just put it in the
+	 * right place; we don't want to shuffle the already-sorted
+	 * messages.
+	 */
+	if(mbox.nmesg == mbox.mesgsz){
+		mbox.mesgsz *= 2;
+		mbox.mesg = erealloc(mbox.mesg, mbox.mesgsz*sizeof(Mesg*));
+	}
+	if(ins)
+		idx = slotfor(m);
+	else
+		idx = mbox.nmesg;
+	memmove(&mbox.mesg[idx + 1], &mbox.mesg[idx], (mbox.nmesg - idx)*sizeof(Mesg*));
+	mbox.mesg[idx] = m;
+	mbox.nmesg++;
+	if(m->messageid == nil)
+		return;
+
+	/* grow hash table, or squeeze out deadwood */
+	if(mbox.hashsz <= 2*(mbox.nmesg + mbox.ndead)){
+		oldsz = mbox.hashsz;
+		oldh = mbox.hash;
+		if(mbox.hashsz <= 2*mbox.nmesg)
+			mbox.hashsz *= 2;
+		mbox.ndead = 0;
+		mbox.hash = emalloc(mbox.hashsz*sizeof(Mesg*));
+		for(i = 0; i < oldsz; i++){
+			if((o = oldh[i]) == nil)
+				continue;
+			mbox.hash[o->hash % mbox.hashsz] = o;
+		}
+		free(oldh);
+	}
+	i = m->hash % mbox.hashsz;
+	while(1){
+		e = mbox.hash[i % mbox.hashsz];
+		if(e == nil || e == &dead)
+			break;
+		i = (i + 1) % mbox.hashsz;
+	}
+	mbox.hash[i] = m;
+}
+
+static Mesg *
+placeholder(char *msgid, vlong time, int ins)
+{
+	Mesg *m;
+
+	m = emalloc(sizeof(Mesg));
+	m->state |= Sdummy|Stoplev;
+	m->messageid = estrdup(msgid);
+	m->hash = strhash(msgid);
+	m->time = time;
+	addmesg(m, ins);
+	return m;
+}
+
+static Mesg*
+change(char *name, char *digest)
+{
+	Mesg *m;
+	char *f;
+
+	if((m = mesglookup(name, digest)) == nil)
+		return nil;
+	if((f = rslurp(m, "flags", nil)) == nil)
+		return nil;
+	free(m->mflags);
+	m->mflags = f;
+	m->flags &= ~(Fdel|Fseen|Fresp);
+	if(strchr(m->mflags, 'd')) m->flags |= Fdel;
+	if(strchr(m->mflags, 's')) m->flags |= Fseen;
+	if(strchr(m->mflags, 'a')) m->flags |= Fresp;
+	return m;
+}
+
+static Mesg*
+delete(char *name, char *digest)
+{
+	Mesg *m;
+
+	if((m = mesglookup(name, digest)) == nil)
+		return nil;
+	m->flags |= Fdel;
+	return m;
+}
+
+static Mesg*
+load(char *name, char *digest, int ins)
+{
+	Mesg *m, *p;
+	int d;
+
+	if(strncmp(name, mbox.path, strlen(mbox.path)) == 0)
+		name += strlen(mbox.path);
+	if((m = mesgload(name)) == nil)
+		goto error;
+
+	if(digest != nil && strcmp(digest, m->digest) != 0)
+		goto error;
+	/* if we already have a dummy, populate it */
+	d = 1;
+	p = lookupid(m->messageid);
+	if(p != nil && (p->state & Sdummy)){
+		d = p->nsub + 1;
+		m->child = p->child;
+		m->nchild = p->nchild;
+		m->nsub = p->nsub;
+		mesgclear(p);
+		memcpy(p, m, sizeof(*p));
+		free(m);
+		m = p;
+
+	}else{
+		/*
+		 * if we raced a notify and a mailbox load, we
+		 * can get duplicate load requests for the same
+		 * name in the mailbox.
+		 */
+		if(p != nil && strcmp(p->name, m->name) == 0)
+			goto error;
+		addmesg(m, ins);
+	}
+
+	if(!threadsort || m->inreplyto == nil || ideq(m->messageid, m->inreplyto)){
+		m->state |= Stoplev;
+		return m;
+	}
+	p = lookupid(m->inreplyto);
+	if(p == nil)
+		p = placeholder(m->inreplyto, m->time, ins);
+	if(!addchild(p, m, d))
+		m->state |= Stoplev;
+	return m;
+error:
+	mesgfree(m);
+	return nil;
+}
+
+void
+mbredraw(Mesg *m, int add, int rec)
+{
+	Biobuf *bfd;
+	int ln, depth;
+
+	ln = mesglineno(m, &depth);
+	fprint(mbox.addr, "%d%s", ln, add?"-#0":"");
+	bfd = bwindata(&mbox, OWRITE);
+	showmesg(bfd, m, depth, rec);
+	Bterm(bfd);
+
+	/* highlight the redrawn message */
+	fprint(mbox.addr, "%d%s", ln, add ? "-#0" : "");
+	fprint(mbox.ctl, "dot=addr\n");
+}
+
+static void
+mbload(void)
+{
+	int i, n, fd;
+	Dir *d;
+
+	mbox.mesgsz = 128;
+	mbox.hashsz = 128;
+	mbox.mesg = emalloc(mbox.mesgsz*sizeof(Mesg*));
+	mbox.hash = emalloc(mbox.hashsz*sizeof(Mesg*));
+	mbox.path = esmprint("%s/%s/", maildir, mailbox);
+	cleanname(mbox.path);
+	n = strlen(mbox.path);
+	if(mbox.path[n - 1] != '/')
+		mbox.path[n] = '/';
+	if((fd = open(mbox.path, OREAD)) == -1)
+		sysfatal("%s: open: %r", mbox.path);
+	while(1){
+		n = dirread(fd, &d);
+		if(n == -1)
+			sysfatal("%s read: %r", mbox.path);
+		if(n == 0)
+			break;
+		for(i = 0; i < n; i++)
+			if(strcmp(d[i].name, "ctl") != 0)
+				load(d[i].name, nil, 0);
+		free(d);
+	}
+	qsort(mbox.mesg, mbox.nmesg, sizeof(Mesg*), cmpmesg);	
+}
+
+static char*
+getflag(Mesg *m)
+{
+	char* flag;
+
+	flag = "★";
+	if(m->flags & Fseen)	flag = " ";
+	if(m->flags & Fresp)	flag = "←";
+	if(m->flags & Fdel)	flag = "∉";
+	if(m->flags & Ftodel)	flag = "∉";
+	return flag;
+}
+
+static void
+printw(Biobuf *bp, char *s, int width)
+{
+	char *dots;
+
+	if(width <= 0)
+		Bprint(bp, "%s", s);
+	else{
+		dots = "";
+		if(utflen(s) > width){
+			width -= 1;
+			dots = "…";
+		}
+		Bprint(bp, "%*.*s%s", -width, width, s, dots);
+	}
+}
+
+/*
+ * Message format string:
+ * ======================
+ * %s: subject
+ * %f: from address
+ * %F: name + from address
+ * %t: to address
+ * %c: CC address
+ * %r: replyto address
+ * %[...]: string to display for child messages
+ * %{...}: date format string
+ */
+static void
+fmtmesg(Biobuf *bp, char *fmt, Mesg *m, int depth)
+{
+	char *p, *e, buf[64];
+	int width, i, indent;
+	Tm tm;
+
+	Bprint(bp, "%-6s\t%s ", m->name, getflag(m));
+	for(p = fmt; *p; p++){
+		if(*p != '%'){
+			Bputc(bp, *p);
+			continue;
+		}
+		p++;
+		width = 0;
+		indent = 0;
+		while(*p == '>'){
+			p++;
+			indent++;
+		}
+		while('0'<=*p && *p<='9')
+			width = width * 10 + *p++ - '0';
+		for(i = 0; indent && i < depth; i++){
+			Bputc(bp, '\t');
+			width -= 4;
+			if(indent == 1)
+				break;
+		}
+		switch(*p){
+		case '%':
+			Bprint(bp, "%%");
+			break;
+		case 'i':
+			if(depth > 0)
+				depth = 1;
+		case 'I':
+			for(i = 0; i < depth; i++){
+				if(width>0)
+					Bprint(bp, "%*s", width, "");
+				else
+					Bprint(bp, "\t");
+			}
+			break;
+		case 's':
+			printw(bp, m->subject, width);
+			break;
+		case 'f':
+			printw(bp, m->from, width);
+			break;
+		case 'F':
+			printw(bp, m->fromcolon, width);
+			break;
+		case 't':
+			printw(bp, m->to, width);
+			break;
+		case 'c':
+			printw(bp, m->cc, width);
+			break;
+		case 'r':
+			printw(bp, m->replyto, width);
+			break;
+		case '[':
+			p++;
+			if((e = strchr(p, ']')) == nil)
+				sysfatal("missing closing '}' in %%{");
+			if(e - p >= sizeof(buf) - 1)
+				sysfatal("%%{} contents too long");
+			snprint(buf, sizeof(buf), "%.*s", (int)(e - p), p);
+			if(depth > 0)
+				Bprint(bp, "%s", buf);
+			p = e;
+			break;
+		case '{':
+			p++;
+			if((e = strchr(p, '}')) == nil)
+				sysfatal("missing closing '}' in %%{");
+			if(e - p >= sizeof(buf) - 1)
+				sysfatal("%%{} contents too long");
+			snprint(buf, sizeof(buf), "%.*s", (int)(e - p), p);
+			tmtime(&tm, m->time, nil);
+			Bprint(bp, "%τ", tmfmt(&tm, buf));
+			p = e;
+			break;
+		default:
+			sysfatal("invalid directive '%%%c' in format string", *p);
+			break;
+		}
+	}
+	Bputc(bp, '\n');
+}
+
+
+static void
+showmesg(Biobuf *bfd, Mesg *m, int depth, int recurse)
+{
+	int i;
+
+	if(!(m->state & Sdummy)){
+		fmtmesg(bfd, listfmt, m, depth);
+		depth++;
+	}
+	if(recurse && mbox.view != Vflat)
+		for(i = 0; i < m->nchild; i++)
+			showmesg(bfd, m->child[i], depth, recurse);
+}
+
+static void
+mark(char **f, int nf, char *fstr, int flags, int add)
+{
+	char *sel, *p, *q, *e, *path;
+	int i, q0, q1, fd;
+	Mesg *m;
+
+	if(flags == 0)
+		return;
+	wingetsel(&mbox, &q0, &q1);
+	if(nf == 0){
+		sel = winreadsel(&mbox);
+		for(p = sel; p != nil; p = e){
+			if((e = strchr(p, '\n')) != nil)
+				*e++ = 0;
+			if(!matchmesg(&mbox, p))
+				continue;
+			if((q = strchr(p, '/')) != nil)
+				q[1] = 0;
+			if((m = mesglookup(p, nil)) != nil){
+				if(add)
+					m->flags |= flags;
+				else
+					m->flags &= ~flags;
+				if(fstr != nil && strlen(fstr) != 0){
+					path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
+					if((fd = open(path, OWRITE)) != -1){
+						fprint(fd, fstr);
+						close(fd);
+					}
+					free(path);
+				}
+				mbredraw(m, 0, 0);
+			}
+		}
+		free(sel);
+	}else for(i = 0; i < nf; i++){
+		if((m = mesglookup(f[i], nil)) != nil){
+			m->flags |= flags;
+			mbredraw(m, 0, 0);
+		}
+	}
+	winsetsel(&mbox, q0, q1);
+}
+
+static void
+mbmark(char **f, int nf)
+{
+	int add, flg;
+
+	if(nf == 0){
+		fprint(2, "missing fstr");
+		return;
+	}
+	if((flg = mesgflagparse(f[0], &add)) == -1){
+		fprint(2, "Mark: invalid flags %s\n", f[0]);
+		return;
+	}
+	mark(f+1, nf-1, f[0], flg, add);
+}
+
+static void
+relinkmsg(Mesg *p, Mesg *m)
+{
+	Mesg *c, *pp;
+	int i, j;
+
+	/* remove child, preserving order */
+	j = 0;
+	for(i = 0; p && i < p->nchild; i++){
+		if(p->child[i] != m)
+			p->child[j++] = p->child[i];
+	}
+	p->nchild = j;
+	for(pp = p; pp != nil; pp = pp->parent)
+		pp->nsub -= m->nsub + 1;
+
+	/* reparent children */
+	for(i = 0; i < m->nchild; i++){
+		c = m->child[i];
+		c->parent = nil;
+		addchild(p, c, c->nsub + 1);
+	}
+}
+
+static void
+mbflush(char **, int)
+{
+	int i, j, ln, fd;
+	char *path;
+	Mesg *m, *p;
+
+	path = estrjoin(maildir, "/ctl", nil);
+	fd = open(path, OWRITE);
+	free(path);
+	if(fd == -1)
+		sysfatal("open mbox: %r");
+	for(i = 0; i < mbox.nmesg; i++){
+		m = mbox.mesg[i];
+		p = m->parent;
+		if(m->state & (Sopen|Szap) || (m->flags & (Fdel|Ftodel)) == 0)
+			continue;
+
+		ln = mesglineno(m, nil);
+		fprint(mbox.addr, "%d,%d", ln, ln+m->nsub);
+		write(mbox.data, "", 0);
+		if(m->flags & Ftodel)
+			fprint(fd, "delete %s %d", mailbox, atoi(m->name));
+
+		removeid(m);
+		m->state |= Szap;
+		if(p == nil && m->nsub != 0){
+			p = placeholder(m->messageid, m->time, 1);
+			p->nsub = m->nsub + 1;
+		}
+		if(p != nil)
+			relinkmsg(p, m);
+		for(j = 0; j < m->nchild; j++)
+			mbredraw(m->child[j], 1, 1);
+ 	}
+
+	for(i = 0, j = 0; i < mbox.nmesg; i++){
+		m = mbox.mesg[i];
+		if((m->state & Szap) != 0)
+			mesgfree(m);
+		else
+			mbox.mesg[j++] = m;
+	}
+	mbox.nmesg = j;
+		
+	close(fd);
+	fprint(mbox.ctl, "clean\n");
+}
+
+static void
+mbcompose(char **, int)
+{
+	compose("", nil, 0);
+}
+
+static void
+delmesg(char **f, int nf)
+{
+	mark(f, nf, nil, Ftodel, 1);
+}
+
+static void
+undelmesg(char **f, int nf)
+{
+	mark(f, nf, nil, Ftodel, 0);
+}
+
+static void
+showlist(void)
+{
+	Biobuf *bfd;
+	Mesg *m;
+	int i;
+
+	bfd = bwindata(&mbox, OWRITE);
+	for(i = 0; i < mbox.nmesg; i++){
+		m = mbox.mesg[i];
+		if(mbox.view == Vflat || m->state & (Sdummy|Stoplev))
+			showmesg(bfd, m, 0, 1);
+	}
+	Bterm(bfd);
+	fprint(mbox.addr, "0");
+	fprint(mbox.ctl, "dot=addr\n");
+	fprint(mbox.ctl, "show\n");
+}
+
+static void
+quitall(char **, int)
+{
+	Mesg *m;
+	Comp *c;
+
+	if(mbox.nopen > 0 && !mbox.canquit){
+		fprint(2, "Del: %d open messages\n", mbox.nopen);
+		mbox.canquit = 1;
+		return;
+	}
+	for(m = mbox.openmesg; m != nil; m = m->qnext)
+		fprint(m->ctl, "del\n");
+	for(c = mbox.opencomp; c != nil; c = c->qnext)
+		fprint(c->ctl, "del\n");
+	fprint(mbox.ctl, "del\n");
+	threadexitsall(nil);
+}
+
+/*
+ * shuffle a message to the right location
+ * in the list without doing a full sort.
+ */
+static void
+reinsert(Mesg *m)
+{
+	int i, idx;
+
+	idx = slotfor(m);
+	for(i = idx; i < mbox.nmesg; i++)
+		if(mbox.mesg[i] == m)
+			break;
+	memmove(&mbox.mesg[idx + 1], &mbox.mesg[idx], (i - idx)*sizeof(Mesg*));
+	mbox.mesg[idx] = m;
+}
+
+static void
+changemesg(Plumbmsg *pm)
+{
+	char *digest, *action;
+	Mesg *m, *r;
+	int ln, nr;
+
+	digest = plumblookup(pm->attr, "digest");
+	action = plumblookup(pm->attr, "mailtype");
+	if(digest == nil || action == nil)
+		return;
+	if(strcmp(action, "new") == 0){
+		if((m = load(pm->data, digest, 1)) == nil)
+			return;
+		for(r = m; r->parent != nil; r = r->parent)
+			/* nothing */;
+		/* Bump whole thread up in list */
+		if(r->nsub > 0){
+			ln = mesglineno(r, nil);
+			nr = r->nsub-1;
+			if(!(r->state & Sdummy))
+				nr++;
+			/*
+			 * We can end up with an empty container
+			 * in an edge case.
+			 *
+			 * Imagine we have a dummy message with
+			 * a child, and that child gets deleted,
+			 * then a new message comes in replying
+			 * to that dummy.
+			 *
+			 * In this case, r->nsub == 1 due to the
+			 * newly added message, so nr=0.
+			 * in that case, skip the redraw, and
+			 * reinsert the dummy in the right place.
+			 */
+			if(nr > 0){
+				fprint(mbox.addr, "%d,%d", ln, ln+nr-1);
+				write(mbox.data, "", 0);
+			}
+			reinsert(r);
+		}
+		mbredraw(r, 1, 1);
+	}else if(strcmp(action, "delete") == 0){
+		if((m = delete(pm->data, digest)) != nil)
+			mbredraw(m, 0, 0);
+	}else if(strcmp(action, "modify") == 0){
+		if((m = change(pm->data, digest)) != nil)
+			mbredraw(m, 0, 0);
+	}
+}
+
+static void
+viewmesg(Plumbmsg *pm)
+{
+	Mesg *m;
+	m = mesgopen(pm->data, plumblookup(pm->attr, "digest"));
+	if(m != nil){
+		fprint(mbox.addr, "%d", mesglineno(m, nil));
+		fprint(mbox.ctl, "dot=addr\n");
+		fprint(mbox.ctl, "show\n");
+	}
+}
+
+static void
+redraw(char **, int)
+{
+	fprint(mbox.addr, ",");
+	showlist();
+}
+
+static void
+nextunread(char **, int)
+{
+	fprint(mbox.ctl, "addr=dot\n");
+	fprint(mbox.addr, "/^[0-9]+\\/ *\t★.*");
+	fprint(mbox.ctl, "dot=addr\n");
+	fprint(mbox.ctl, "show\n");
+}
+
+Fn mboxfn[] = {
+	{"Put",	mbflush},
+	{"Mail", mbcompose},
+	{"Delmesg", delmesg},
+	{"Undelmesg", undelmesg},
+	{"Del", quitall},
+	{"Redraw", redraw},
+	{"Next", nextunread},
+	{"Mark", mbmark},
+#ifdef NOTYET
+	{"Filter", filter},
+	{"Get", mbrefresh},
+#endif
+	{nil}
+};
+
+static void
+doevent(Event *ev)
+{
+	char *f[32];
+	int nf;
+	Fn *p;
+
+	if(ev->action != 'M')
+		return;
+	switch(ev->type){
+	case 'l':
+	case 'L':
+		if(matchmesg(&mbox, ev->text))
+			if(mesgopen(ev->text, nil) != nil)
+				break;
+		winreturn(&mbox, ev);
+		break;
+	case 'x':
+	case 'X':
+		if((nf = tokenize(ev->text, f, nelem(f))) == 0)
+			return;
+		for(p = mboxfn; p->fn != nil; p++)
+			if(strcmp(p->name, f[0]) == 0 && p->fn != nil){
+				p->fn(&f[1], nf - 1);
+				break;
+			}
+		if(p->fn == nil)
+			winreturn(&mbox, ev);
+		else if(p->fn != quitall)
+			mbox.canquit = 0;
+		break;
+	}
+}
+
+static void
+execlog(void*)
+{
+	Waitmsg *w;
+
+	while(1){
+		w = recvp(cwait);
+		if(w->msg[0] != 0)
+			fprint(2, "%d: %s\n", w->pid, w->msg);
+		free(w);
+	}
+} 
+
+static void
+mbmain(void *cmd)
+{
+	Event *ev;
+	Plumbmsg *psee, *pshow, *psend;
+
+	Alt a[] = {
+	[Cevent]	= {mbox.event, &ev, CHANRCV},
+	[Cseemail]	= {mbox.see, &psee, CHANRCV},
+	[Cshowmail]	= {mbox.show, &pshow, CHANRCV},
+	[Csendmail]	= {mbox.send, &psend, CHANRCV},
+	[Nchan]		= {nil,	nil, CHANEND},
+	};
+
+	threadsetname("mbox %s", mbox.path);
+	wininit(&mbox, mbox.path);
+	wintagwrite(&mbox, "Put Mail Delmesg Undelmesg Next ");
+	showlist();
+	fprint(mbox.ctl, "dump %s\n", cmd);
+	fprint(mbox.ctl, "clean\n");
+	proccreate(eventread, nil, Stack);
+	while(1){
+		switch(alt(a)){
+		case Cevent:
+			doevent(ev);
+			free(ev);
+			break;
+		case Cseemail:
+			changemesg(psee);
+			plumbfree(psee);
+			break;
+		case Cshowmail:
+			viewmesg(pshow);
+			plumbfree(pshow);
+			break;
+		case Csendmail:
+			compose(psend->data, nil, 0);
+			plumbfree(psend);
+			break;
+		}
+	}
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-T] [-m mailfs] [-s] [-f format] [mbox]\n", argv0);
+	exits("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	Fmt fmt;
+	char *cmd;
+	int i;
+
+	mbox.view = Vgroup;
+	doquote = needsrcquote;
+	quotefmtinstall();
+	tmfmtinstall();
+
+	fmtstrinit(&fmt);
+	for(i = 0; i < argc; i++)
+		fmtprint(&fmt, "%q ", argv[i]);
+	cmd = fmtstrflush(&fmt);
+	if(cmd == nil)
+		sysfatal("out of memory");
+
+	ARGBEGIN{
+	case 'm':
+		maildir = EARGF(usage());
+		break;
+	case 'T':
+		mbox.view = Vflat;
+		break;
+	case 's':
+		sender++;
+		break;
+	case 'f':
+		listfmt = EARGF(usage());
+		break;
+	case 'O':
+		savebox = nil;
+		break;
+	case 'o':
+		savebox = EARGF(usage());
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	if(argc > 1)
+		usage();
+	if(argc == 1)
+		mailbox = argv[0];
+
+	mesgpat = regcomp("([0-9]+)(/.*)?");
+	cwait = threadwaitchan();
+
+	/* open these early so we won't miss messages while loading */
+	mbox.event = chancreate(sizeof(Event*), 0);
+	mbox.see = chancreate(sizeof(Plumbmsg*), 0);
+	mbox.show = chancreate(sizeof(Plumbmsg*), 0);
+	mbox.send = chancreate(sizeof(Plumbmsg*), 0);
+
+	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
+	plumbseemailfd = plumbopen("seemail", OREAD|OCEXEC);
+	plumbshowmailfd = plumbopen("showmail", OREAD|OCEXEC);
+
+	mbload();
+	proccreate(plumbseemail, nil, Stack);
+	proccreate(plumbshowmail, nil, Stack);
+
+	/* avoid duplicate sends when multiple mailboxes are open */
+	if(sender || strcmp(mailbox, "mbox") == 0){
+		plumbsendmailfd = plumbopen("sendmail", OREAD|OCEXEC);
+		proccreate(plumbsendmail, nil, Stack);
+	}
+	threadcreate(execlog, nil, Stack);
+	threadcreate(mbmain, cmd, Stack);
+	threadexits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/mesg.c
@@ -1,0 +1,545 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+#define Datefmt		"?WWW, ?MMM ?DD hh:mm:ss ?Z YYYY"
+
+typedef struct Fn	Fn;
+
+struct Fn {
+	char *name;
+	void (*fn)(Mesg *, char **, int);
+};
+
+void
+mesgclear(Mesg *m)
+{
+	int i;
+
+	for(i = 0; i < m->nparts; i++)
+		mesgclear(m->parts[i]);
+	free(m->name);
+	free(m->from);
+	free(m->to);
+	free(m->cc);
+	free(m->replyto);
+	free(m->date);
+	free(m->subject);
+	free(m->type);
+	free(m->disposition);
+	free(m->messageid);
+	free(m->filename);
+	free(m->digest);
+	free(m->mflags);
+	free(m->fromcolon);
+}
+
+void
+mesgfree(Mesg *m)
+{
+	if(m == nil)
+		return;
+	mesgclear(m);
+	free(m);
+}
+
+static char*
+line(char *data, char **pp, int z)
+{
+	char *p, *q;
+
+	for(p=data; *p!='\0' && *p!='\n'; p++)
+		;
+	if(*p == '\n')
+		*pp = p+1;
+	else
+		*pp = p;
+	if(z && p == data)
+		return nil;
+	q = emalloc(p-data + 1);
+	memmove(q, data, p-data);
+	return q;
+}
+
+static char*
+fc(Mesg *m, char *s)
+{
+	char *r;
+
+	if(s != nil && strlen(m->from) != 0){
+		r = smprint("%s <%s>", s, m->from);
+		free(s);
+		return r;
+	}
+	if(m->from != nil)
+		return estrdup(m->from);
+	if(s != nil)
+		return s;
+	return estrdup("??");
+}
+
+Mesg*
+mesgload(char *name)
+{
+	char *info, *p;
+	int ninfo;
+	Mesg *m;
+	Tm tm;
+
+	m = emalloc(sizeof(Mesg));
+	m->name = estrjoin(name, "/", nil);
+	if((info = rslurp(m, "info", &ninfo)) == nil){
+		free(m->name);
+		free(m);
+		return nil;
+	}
+
+	p = info;
+	m->from = line(p, &p, 0);
+	m->to = line(p, &p, 0);
+	m->cc = line(p, &p, 0);
+	m->replyto = line(p, &p, 1);
+	m->date = line(p, &p, 0);
+	m->subject = line(p, &p, 0);
+	m->type = line(p, &p, 1);
+	m->disposition = line(p, &p, 1);
+	m->filename = line(p, &p, 1);
+	m->digest = line(p, &p, 1);
+	/* m->bcc = */ free(line(p, &p, 1));
+	m->inreplyto = line(p, &p, 1);
+	/* m->date = */ free(line(p, &p, 1));
+	/* m->sender = */ free(line(p, &p, 1));
+	m->messageid = line(p, &p, 0);
+	/* m->lines = */ free(line(p, &p, 1));
+	/* m->size = */ free(line(p, &p, 1));
+	m->mflags = line(p, &p, 0);
+	/* m->fileid = */ free(line(p, &p, 1));
+	m->fromcolon = fc(m, line(p, &p, 1));
+	free(info);
+
+	m->flags = 0;
+	if(strchr(m->mflags, 'd')) m->flags |= Fdel;
+	if(strchr(m->mflags, 's')) m->flags |= Fseen;
+	if(strchr(m->mflags, 'a')) m->flags |= Fresp;
+
+	m->time = time(nil);
+	if(tmparse(&tm, Datefmt, m->date, nil, nil) != nil)
+		m->time = tmnorm(&tm);
+	m->hash = 0;
+	if(m->messageid != nil)
+		m->hash = strhash(m->messageid);
+	return m;
+}
+
+static Mesg*
+readparts(Mesg *r, Mesg *m)
+{
+	char *dpath, *apath;
+	int n, i, dfd;
+	Mesg *a, *sub;
+	Dir *d;
+
+	if(m->body != nil)
+		return m->body;
+
+	dpath = estrjoin(mbox.path, m->name, nil);
+	dfd = open(dpath, OREAD);
+	free(dpath);
+	if(dfd == -1)
+		return m;
+
+	n = dirreadall(dfd, &d);
+	close(dfd);
+	if(n == -1)
+		sysfatal("%s read: %r", mbox.path);
+
+	m->body = nil;
+	for(i = 0; i < n; i++){
+		if(d[i].qid.type != QTDIR)
+			continue;
+
+		apath = estrjoin(m->name, d[i].name, nil);
+		a = mesgload(apath);
+		free(apath);
+		if(a == nil)
+			continue;
+		if(strncmp(a->type, "multipart/", strlen("multipart/")) == 0){
+			sub = readparts(r, a);
+			if(sub != a)
+				m->body = sub;
+			continue;
+		} 
+		if(r->nparts >= r->xparts)
+			r->parts = erealloc(r->parts, (2 + r->nparts*2)*sizeof(Mesg*));
+		r->parts[r->nparts++] = a;
+		if(r->body == nil && strcmp(a->type, "text/plain") == 0)
+			r->body = a;
+		else if(r->body == nil && strcmp(a->type, "text/html") == 0)
+			r->body = a;
+	}
+	free(d);
+	if(m->body == nil)
+		m->body = m;
+	return m->body;
+}
+
+static void
+execfmt(void *pm)
+{
+	Mesg *m;
+
+	m = pm;
+	rfork(RFFDG);
+	dup(m->fd[1], 1);
+	close(m->fd[0]);
+	close(m->fd[1]);
+	procexecl(m->sync, "/bin/htmlfmt", "htmlfmt", "-a", "-cutf-8", m->path, nil);
+}
+
+static int
+htmlfmt(Mesg *m, char *path)
+{
+	if(pipe(m->fd) == -1)
+		sysfatal("pipe: %r");
+	m->sync = chancreate(sizeof(ulong), 0);
+	m->path = path;
+	procrfork(execfmt, m, Stack, RFNOTEG);
+	recvul(m->sync);
+	chanfree(m->sync);
+	close(m->fd[1]);
+	return m->fd[0];
+}
+
+static void
+copy(Biobuf *wfd, Biobuf *rfd)
+{
+	char *buf;
+	int n;
+
+	buf = emalloc(Bufsz);
+	while(1){
+		n = Bread(rfd, buf, Bufsz);
+		if(n <= 0)
+			break;
+		if(Bwrite(wfd, buf, n) != n)
+			break;
+	}
+	free(buf);
+}
+
+static int
+mesgshow(Mesg *m)
+{
+	char *path, *home, *name, *suff;
+	Biobuf *rfd, *wfd;
+	Mesg *a;
+	int i;
+
+	if((wfd = bwinopen(m, "body", OWRITE)) == nil)
+		return -1;
+	if(m->parent != nil || m->nchild != 0) {
+		Bprint(wfd, "Thread:");
+		if(m->parent && !(m->parent->state & Sdummy))
+			Bprint(wfd, " ↑ %s", m->parent->name);
+		for(i = 0; i < m->nchild; i++)
+			Bprint(wfd, " ↓ %s", m->child[i]->name);
+		Bprint(wfd, "\n");
+	}
+	Bprint(wfd, "From: %s\n", m->fromcolon);
+	Bprint(wfd, "To:   %s\n", m->to);
+	Bprint(wfd, "Date: %s\n", m->date);
+	Bprint(wfd, "Subject: %s\n\n", m->subject);
+
+	rfd = mesgopenbody(m);
+	if(rfd != nil){
+		copy(wfd, rfd);
+		Bterm(rfd);
+	}
+
+	home = getenv("home");
+	if(m->nparts != 0)
+		Bprint(wfd, "\n");
+	for(i = 0; i < m->nparts; i++){
+		a = m->parts[i];
+		name = a->name;
+		if(strncmp(a->name, m->name, strlen(m->name)) == 0)
+			name += strlen(m->name);
+		if(a->disposition != nil
+		&& strcmp(a->disposition, "inline") == 0
+		&& strcmp(a->type, "text/plain") == 0){
+			if(a == m || a == m->body)
+				continue;
+			Bprint(wfd, "\n===> %s (%s)\n", name, a->type);
+			path = estrjoin(mbox.path, a->name, "body", nil);
+			if((rfd = Bopen(path, OREAD)) != nil){
+				copy(wfd, rfd);
+				Bterm(rfd);
+			}
+			free(path);
+			continue;
+		}
+		Bprint(wfd, "\n===> %s (%s)\n", name, a->type);
+		name = a->filename;
+		if(name == nil)
+			name = "body";
+		if((suff = strchr(name, '.')) == nil)
+			suff = "";
+		Bprint(wfd, "\tcp %s%sbody%s %s/%s\n", mbox.path, a->name, suff, home, name);
+		continue;
+	}
+	Bterm(wfd);
+	free(home);
+	fprint(m->ctl, "clean\n");
+	return 0;
+}
+
+static void
+reply(Mesg *m, char **f, int nf)
+{
+	if(nf >= 1 &&  strcmp(f[0], "all") == 0)
+		compose(m->replyto, m, 1);
+	else
+		compose(m->replyto, m, 0);
+}
+
+static void
+delmesg(Mesg *m, char **, int nf)
+{
+	if(nf != 0){
+		fprint(2, "Delmesg: too many args\n");
+		return;
+	}
+	m->flags |= Ftodel;
+	m->quitting = 1;
+	mbredraw(m, 0, 0);
+}
+
+static void
+markone(Mesg *m, char **f, int nf)
+{
+	int add, flg, fd;
+	char *path;
+
+	if(nf != 1){
+		fprint(2, "Mark: invalid arguments");
+		return;
+	}
+
+	if((flg = mesgflagparse(f[0], &add)) == -1){
+		fprint(2, "Mark: invalid flags %s\n", f[0]);
+		return;
+	}
+	if(add)
+		m->flags |= flg;
+	else
+		m->flags &= ~flg;
+	if(strlen(f[0]) != 0){
+		path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
+		if((fd = open(path, OWRITE)) != -1){
+			fprint(fd, f[0]);
+			close(fd);
+		}
+		free(path);
+	}
+	mbredraw(m, 0, 0);
+}
+
+
+static void
+mesgquit(Mesg *m, char **, int)
+{
+	if(fprint(m->ctl, "del\n") == -1)
+		return;
+	m->quitting = 1;
+	m->open = 0;
+}
+
+static Fn mesgfn[] = {
+	{"Reply",	reply},
+	{"Delmesg",	delmesg},
+	{"Del", 	mesgquit},
+	{"Mark",	markone},
+#ifdef NOTYET
+	{"Save",	nil},
+#endif
+	{nil}
+};
+
+static void
+mesgmain(void *mp)
+{
+	char *path, *f[32];
+	Event ev;
+	Mesg *m, **pm;
+	Fn *p;
+	int nf;
+
+	m = mp;
+	m->quitting = 0;
+	m->qnext = mbox.openmesg;
+	mbox.openmesg = m;
+
+	path = estrjoin(mbox.path, m->name, nil);
+	wininit(m, path);
+	free(path);
+
+	wintagwrite(m, "Reply all Delmesg Save  ");
+	mesgshow(m);
+	fprint(m->ctl, "clean\n");
+	mbox.nopen++;
+	while(!m->quitting){
+		if(winevent(m, &ev) != 'M')
+			continue;
+		if(strcmp(ev.text, "Del") == 0)
+			break;
+		switch(ev.type){
+		case 'l':
+		case 'L':
+			if(matchmesg(m, ev.text))
+				mesgopen(ev.text, nil);
+			else
+				winreturn(m, &ev);
+			break;
+		case 'x':
+		case 'X':
+			if((nf = tokenize(ev.text, f, nelem(f))) == 0)
+				continue;
+			for(p = mesgfn; p->fn != nil; p++){
+				if(strcmp(p->name, f[0]) == 0 && p->fn != nil){
+					p->fn(m, &f[1], nf - 1);
+					break;
+				}
+			}
+			if(p->fn == nil)
+				winreturn(m, &ev);
+			break;
+		}
+	}
+	for(pm = &mbox.openmesg; *pm != nil; pm = &(*pm)->qnext)
+		if(*pm == m){
+			*pm = m->qnext;
+			break;
+		}
+	mbox.nopen--;
+	m->qnext = nil;
+	m->state &= ~Sopen;
+	winclose(m);
+	threadexits(nil);
+}
+
+int
+mesgflagparse(char *fstr, int *add)
+{
+	int flg;
+
+	flg = 0;
+	*add = (*fstr == '+');
+	if(*fstr == '-' || *fstr == '+')
+		fstr++;
+	for(; *fstr; fstr++){
+		switch(*fstr){
+		case 'a':
+			flg |= Fresp;
+			break;
+		case 's':
+			flg |= Fseen;
+			break;
+		case 'D':
+			flg |= Ftodel;
+			memcpy(fstr, fstr +1, strlen(fstr));
+			break;
+		default:
+			fprint(2, "unknown flag %c", *fstr);
+			return -1;
+		}
+	}
+	return flg;
+}
+
+void
+mesgpath2name(char *buf, int nbuf, char *name)
+{
+	char *p, *e;
+	int n;
+
+	n = strlen(mbox.path);
+	if(strncmp(name, mbox.path, n) == 0)
+		e = strecpy(buf, buf+nbuf-2, name + n);
+	else
+		e = strecpy(buf, buf+nbuf-2, name);
+	if((p = strchr(buf, '/')) == nil)
+		p = e;
+	p[0] = '/';
+	p[1] = 0;
+}
+
+int
+mesgmatch(Mesg *m, char *name, char *digest)
+{
+	if(!(m->state & Sdummy) && strcmp(m->name, name) == 0)
+		return digest == nil || strcmp(m->digest, digest) == 0;
+	return 0;
+}
+
+Mesg*
+mesglookup(char *name, char *digest)
+{
+	char buf[32];
+	int i;
+
+	mesgpath2name(buf, sizeof(buf), name);
+	for(i = 0; i < mbox.nmesg; i++)
+		if(mesgmatch(mbox.mesg[i], buf, digest))
+			return mbox.mesg[i];
+	return nil;
+}
+
+Mesg*
+mesgopen(char *name, char *digest)
+{
+	Mesg *m;
+	char *path;
+	int fd;
+
+	m = mesglookup(name, digest);
+	if(m == nil || (m->state & Sopen))
+		return nil;
+
+	assert(!(m->state & Sdummy));
+	m->state |= Sopen;
+	if(!(m->flags & Fseen)){
+		m->flags |= Fseen;
+		path = estrjoin(mbox.path, "/", m->name, "/flags", nil);
+		if((fd = open(path, OWRITE)) != -1){
+			fprint(fd, "+s");
+			close(fd);
+		}
+		mbredraw(m, 0, 0);
+		free(path);
+	}
+	threadcreate(mesgmain, m, Stack);
+	return m;
+}
+
+Biobuf*
+mesgopenbody(Mesg *m)
+{
+	char *path;
+	int rfd;
+	Mesg *b;
+
+	b = readparts(m, m);
+	path = estrjoin(mbox.path, b->name, "body", nil);
+	if(strcmp(b->type, "text/html") == 0)
+		rfd = htmlfmt(m, path);
+	else
+		rfd = open(path, OREAD);
+	free(path);
+	if(rfd == -1)
+		return nil;
+	return Bfdopen(rfd, OREAD);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/mkfile
@@ -1,0 +1,14 @@
+</$objtype/mkfile
+
+BIN=/acme/bin/$objtype
+TARG=Mail
+OFILES=\
+	mbox.$O\
+	mesg.$O\
+	comp.$O\
+	util.$O\
+	win.$O
+
+HFILES=mail.h
+
+</sys/src/cmd/mkone
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/util.c
@@ -1,0 +1,140 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+void *
+emalloc(ulong n)
+{
+	void *v;
+	
+	v = mallocz(n, 1);
+	if(v == nil)
+		sysfatal("malloc: %r");
+	setmalloctag(v, getcallerpc(&n));
+	return v;
+}
+
+void *
+erealloc(void *p, ulong n)
+{
+	void *v;
+	
+	v = realloc(p, n);
+	if(v == nil)
+		sysfatal("realloc: %r");
+	setmalloctag(v, getcallerpc(&p));
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	s = strdup(s);
+	if(s == nil)
+		sysfatal("strdup: %r");
+	setmalloctag(s, getcallerpc(&s));
+	return s;
+}
+
+char*
+estrjoin(char *s, ...)
+{
+	va_list ap;
+	char *r, *t, *p, *e;
+	int n;
+
+	va_start(ap, s);
+	n = strlen(s) + 1;
+	while((p = va_arg(ap, char*)) != nil)
+		n += strlen(p);
+	va_end(ap);
+
+	r = emalloc(n);
+	e = r + n;
+	va_start(ap, s);
+	t = strecpy(r, e, s);
+	while((p = va_arg(ap, char*)) != nil)
+		t = strecpy(t, e, p);
+	va_end(ap);
+	return r;
+}
+
+char*
+esmprint(char *fmt, ...)
+{
+	char *s;
+	va_list ap;
+
+	va_start(ap, fmt);
+	s = vsmprint(fmt, ap);
+	va_end(ap);
+	if(s == nil)
+		sysfatal("smprint: %r");
+	setmalloctag(s, getcallerpc(&fmt));
+	return s;
+}
+
+char*
+fslurp(int fd, int *nbuf)
+{
+	int n, sz, r;
+	char *buf;
+
+	n = 0;
+	sz = 128;
+	buf = emalloc(sz);
+	while(1){
+		r = read(fd, buf + n, sz - n);
+		if(r == 0)
+			break;
+		if(r == -1)
+			goto error;
+		n += r;
+		if(n == sz){
+			sz += sz/2;
+			buf = erealloc(buf, sz);
+		}
+	}
+	buf[n] = 0;
+	if(nbuf)
+		*nbuf = n;
+	return buf;
+error:
+	free(buf);
+	return nil;
+}
+
+char *
+rslurp(Mesg *m, char *f, int *nbuf)
+{
+	char *path;
+	int fd;
+	char *r;
+
+	if(m == nil)
+		path = estrjoin(mbox.path, "/", f, nil);
+	else
+		path = estrjoin(mbox.path, "/", m->name, "/", f, nil);
+	fd = open(path, OREAD);
+	free(path);
+	if(fd == -1)
+		return nil;
+	r = fslurp(fd, nbuf);
+	close(fd);
+	return r;
+}
+
+u32int
+strhash(char *s)
+{
+	u32int h, c;
+
+	h = 5381;
+	while(c = *s++ & 0xff)
+		h = ((h << 5) + h) + c;
+	return h;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/Mail/win.c
@@ -1,0 +1,281 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <regexp.h>
+
+#include "mail.h"
+
+static int
+procrd(Biobufhdr *f, void *buf, long len)
+{
+	return ioread(f->aux, f->fid, buf, len);
+}
+
+static int
+procwr(Biobufhdr *f, void *buf, long len)
+{
+	return iowrite(f->aux, f->fid, buf, len);
+}
+
+/*
+ * NB: this function consumes integers with
+ * a trailing space, as generated by acme;
+ * it's not a general purpose number parsing
+ * function.
+ */
+static int
+evgetnum(Biobuf *f)
+{
+	int c, n;
+
+	n = 0;
+	while('0'<=(c=Bgetc(f)) && c<='9')
+		n = n*10+(c-'0');
+	if(c != ' '){
+		werrstr("event number syntax: %c", c);
+		return -1;
+	}
+	return n;
+}
+
+static int
+evgetdata(Biobuf *f, Event *e)
+{
+	int i, n, o;
+	Rune r;
+
+	o = 0;
+	n = evgetnum(f);
+	for(i = 0; i < n; i++){
+		if((r = Bgetrune(f)) == -1)
+			break;
+		o += runetochar(e->text + o, &r);
+	}
+	e->text[o] = 0;
+	return o;
+}
+
+int
+winevent(Win *w, Event *e)
+{
+	int flags;
+
+	flags = 0;
+Again:
+	e->action = Bgetc(w->event);
+	e->type = Bgetc(w->event);
+	e->q0 = evgetnum(w->event);
+	e->q1 = evgetnum(w->event);
+	e->flags = evgetnum(w->event);
+	e->ntext = evgetdata(w->event, e);
+	if(Bgetc(w->event) != '\n'){
+		werrstr("unterminated message");
+		return -1;
+	}
+//fprint(2, "event: %c%c %d %d %x %.*s\n", e->action, e->type, e->q0, e->q1, e->flags, e->ntext, e->text);
+	if(e->flags & 0x2){
+		e->p0 = e->q0;
+		flags = e->flags;
+		goto Again;
+	}
+	e->flags |= flags;
+	return e->action;
+}
+
+void
+winreturn(Win *w, Event *e)
+{
+	if(e->flags & 0x2)
+		fprint(w->revent, "%c%c%d %d\n", e->action, e->type, e->p0, e->p0);
+	else
+		fprint(w->revent, "%c%c%d %d\n", e->action, e->type, e->q0, e->q1);
+}
+
+int
+winopen(Win *w, char *f, int mode)
+{
+	char buf[128];
+	int fd;
+
+	snprint(buf, sizeof(buf), "/mnt/wsys/%d/%s", w->id, f);
+	if((fd = open(buf, mode|OCEXEC)) == -1)
+		sysfatal("open %s: %r", buf);
+	return fd;
+}
+
+Biobuf*
+bwinopen(Win *w, char *f, int mode)
+{
+	char buf[128];
+	Biobuf *bfd;
+
+	snprint(buf, sizeof(buf), "/mnt/wsys/%d/%s", w->id, f);
+	if((bfd = Bopen(buf, mode|OCEXEC)) == nil)
+		sysfatal("open %s: %r", buf);
+	bfd->aux = w->io;
+	Biofn(bfd, (mode == OREAD)?procrd:procwr);
+	return bfd;
+}
+
+Biobuf*
+bwindata(Win *w, int mode)
+{
+	int fd;
+
+	if((fd = dup(w->data, -1)) == -1)
+		sysfatal("dup: %r");
+	return Bfdopen(fd, mode);
+}
+
+void
+wininit(Win *w, char *name)
+{
+	char buf[12];
+
+	w->ctl = open("/mnt/wsys/new/ctl", ORDWR|OCEXEC);
+	if(w->ctl < 0)
+		sysfatal("winopen: %r");
+	if(read(w->ctl, buf, 12)!=12)
+		sysfatal("read ctl: %r");
+	if(fprint(w->ctl, "name %s\n", name) == -1)
+		sysfatal("write ctl: %r");
+	if(fprint(w->ctl, "noscroll\n") == -1)
+		sysfatal("write ctl: %r");
+	w->io = ioproc();
+	w->id = atoi(buf);
+	w->event = bwinopen(w, "event", OREAD);
+	w->revent = winopen(w, "event", OWRITE);
+	w->addr = winopen(w, "addr", ORDWR);
+	w->data = winopen(w, "data", ORDWR);
+	w->open = 1;
+}
+
+void
+winclose(Win *w)
+{
+	if(w->open)
+		fprint(w->ctl, "delete\n");
+	if(w->data != -1)
+		close(w->data);
+	if(w->addr != -1)
+		close(w->addr);
+	if(w->event != nil)
+		Bterm(w->event);
+	if(w->revent != -1)
+		close(w->revent);
+	if(w->io)
+		closeioproc(w->io);
+	if(w->ctl != -1)
+		close(w->ctl);
+	w->ctl = -1;
+	w->data = -1;
+	w->addr = -1;
+	w->event = nil;
+	w->revent = -1;
+	w->io = nil;
+}
+
+void
+wintagwrite(Win *w, char *s)
+{
+	int fd, n;
+
+	n = strlen(s);
+	fd = winopen(w, "tag", OWRITE);
+	if(write(fd, s, n) != n)
+		sysfatal("tag write: %r");
+	close(fd);
+}
+
+int
+winread(Win *w, int q0, int q1, char *data, int ndata)
+{
+	int m, n, nr;
+	char *buf;
+
+	m = q0;
+	buf = emalloc(Bufsz);
+	while(m < q1){
+		n = sprint(buf, "#%d", m);
+		if(write(w->addr, buf, n) != n){
+			fprint(2, "error writing addr: %r");
+			goto err;
+		}
+		n = read(w->data, buf, Bufsz);
+		if(n <= 0){
+			fprint(2, "reading data: %r");
+			goto err;
+		}
+		nr = utfnlen(buf, n);
+		while(m+nr >q1){
+			do; while(n>0 && (buf[--n]&0xC0)==0x80);
+			--nr;
+		}
+		if(n == 0 || n > ndata)
+			break;
+		memmove(data, buf, n);
+		ndata -= n;
+		data += n;
+		*data = 0;
+		m += nr;
+	}
+	free(buf);
+	return 0;
+err:
+	free(buf);
+	return -1;
+}
+
+char*
+winreadsel(Win *w)
+{
+	int n, q0, q1;
+	char *r;
+
+	wingetsel(w, &q0, &q1);
+	n = UTFmax*(q1-q0);
+	r = emalloc(n + 1);
+	if(winread(w, q0, q1, r, n) == -1){
+		free(r);
+		return nil;
+	}
+	return r;
+}
+
+void
+wingetsel(Win *w, int *q0, int *q1)
+{
+	char *e, buf[25];
+
+	fprint(w->ctl, "addr=dot");
+	if(pread(w->addr, buf, 24, 0) != 24)
+		sysfatal("read addr: %r");
+	buf[24] = 0;
+	*q0 = strtol(buf, &e, 10);
+	*q1 = strtol(e, nil, 10);
+}
+
+void
+winsetsel(Win *w, int q0, int q1)
+{
+	fprint(w->addr, "#%d,#%d", q0, q1);
+	fprint(w->ctl, "dot=addr");
+}
+
+int
+matchmesg(Win *, char *text)
+{
+	Resub m[3];
+
+	memset(m, 0, sizeof(m));
+	if(strncmp(text, mbox.path, strlen(mbox.path)) == 0)
+		return strstr(text + strlen(mbox.path), "/body") == nil;
+	else if(regexec(mesgpat, text, m, nelem(m))){
+		if(*m[1].ep == 0 || *m[1].ep == '/'){
+			*m[1].ep = 0;
+			return 1;
+		}
+	}
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/README
@@ -1,0 +1,20 @@
+Once upon a time Upas ran on many versions of Unix.
+This is a partial rewrite to ANSI C specifically for Plan 9.
+It uses's Plan 9's bio library instead of stdio and Plan 9's
+regular expression library.
+
+I've tried to make portability possible but it has
+never been ported.  To port Upas to another system:
+
+	- port Plan 9's libbio library working on that system (already available).
+	- port Plan 9's regexp library working on that system (should just compile).
+	- rewrite common/libsys.c to reflect system calls for that system.  This
+	  file contains all the really system dependent code that differs between
+	  Plan 9 and each Unix.  This includes file management, signal
+	  handling, process control and error handling.
+	- change the important directory trees in common/mail.c to reflect
+	  where you want things like 
+	- get the ARGBEGIN/ARGEND/ARGF macros from Plan 9's libc.h
+	- get the include files correct in common/sys.h
+	- rewrite smtp/mxdial to use the conventions of that system
+	- rewrite runq.c to walk queues on that system.
--- /dev/null
+++ b/sys/src/cmd/upas/alias/aliasmail.c
@@ -1,0 +1,254 @@
+#include "common.h"
+
+/*
+ *  WARNING!  This turns all upper case names into lower case
+ *  local ones.
+ */
+
+static	String	*getdbfiles(void);
+static	int	translate(char*, char**, String*, String*);
+static	int	lookup(String**, String*, String*);
+static	char	*mklower(char*);
+
+static	int	debug;
+static	int	from;
+static	char	*namefiles = "namefiles";
+
+#define dprint(...) if(debug)fprint(2, __VA_ARGS__); else {}
+
+void
+usage(void)
+{
+	fprint(2, "usage: aliasmail [-df] [-n namefile] [names ...]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char *argv[])
+{
+	char *alias, **names, *p;		/* names of this system */
+	int i, rv;
+	String *s, *salias, *files;
+
+	ARGBEGIN {
+	case 'd':
+		debug = 1;
+		break;
+	case 'f':
+		from = 1;
+		break;
+	case 'n':
+		namefiles = EARGF(usage());
+		break;
+	default:
+		usage();
+	} ARGEND
+	if (chdir(UPASLIB) < 0)
+		sysfatal("chdir: %r");
+
+	names = sysnames_read();
+	files = getdbfiles();
+	salias = s_new();
+
+	/* loop through the names to be translated (from standard input) */
+	for(i=0; i<argc; i++) {
+		s = unescapespecial(s_copy(mklower(argv[i])));
+		if(strchr(s_to_c(s), '!') == 0)
+			rv = translate(s_to_c(s), names, files, salias);
+		else
+			rv = -1;
+		alias = s_to_c(salias);
+		if(from){
+			if (rv >= 0 && *alias != '\0'){
+				if(p = strchr(alias, '\n'))
+					*p = 0;
+				if(p = strchr(alias, '!')) {
+					*p = 0;
+					print("%s", alias);
+				} else {
+					if(p = strchr(alias, '@'))
+						print("%s", p+1);
+					else
+						print("%s", alias);
+				}
+			}
+		} else {
+			if (rv < 0 || *alias == '\0')
+				print("local!%s\n", s_to_c(s));
+			else
+				/* this must be a write, not a print */
+				write(1, alias, strlen(alias));
+		}
+		s_free(s);
+	}
+	exits(0);
+}
+
+/* get the list of dbfiles to search */
+static String *
+getdbfiles(void)
+{
+	char *nf;
+	Sinstack *sp;
+	String *files;
+
+	if(from)
+		nf = "fromfiles";
+	else
+		nf = namefiles;
+
+	/* system wide aliases */
+	files = s_new();
+	if ((sp = s_allocinstack(nf)) != 0){
+		while(s_rdinstack(sp, files))
+			s_append(files, " ");
+		s_freeinstack(sp);
+	}
+
+	dprint("files are %s\n", s_to_c(files));
+	return files;
+}
+
+/* loop through the translation files */
+static int
+translate(char *name, char **namev,	String *files, String *alias)
+{
+	int n, rv;
+	String *file, **fullnamev;
+
+	rv = -1;
+	file = s_new();
+
+	dprint("translate(%s, %s, %s)\n", name,
+		s_to_c(files), s_to_c(alias));
+
+	/* create the full name to avoid loops (system!name) */
+	for(n = 0; namev[n]; n++)
+		;
+
+	fullnamev = (String**)malloc(sizeof(String*)*(n+2));
+	n = 0;
+	fullnamev[n++] = s_copy(name);
+	for(; *namev; namev++){
+		fullnamev[n] = s_copy(*namev);
+		s_append(fullnamev[n], "!");
+		s_append(fullnamev[n], name);
+		n++;
+	}
+	fullnamev[n] = 0;
+
+	/* look at system-wide names */
+	s_restart(files);
+	while (s_parse(files, s_restart(file)) != 0)
+		if (lookup(fullnamev, file, alias)==0) {
+			rv = 0;
+			goto out;
+		}
+
+out:
+	for(n = 0; fullnamev[n]; n++)
+		s_free(fullnamev[n]);
+	s_free(file);
+	free(fullnamev);
+	return rv;
+}
+
+/*
+ *  very dumb conversion to bang format
+ */
+static String*
+attobang(String *token)
+{
+	char *p;
+	String *tok;
+
+	p = strchr(s_to_c(token), '@');
+	if(p == 0)
+		return token;
+
+	p++;
+	tok = s_copy(p);
+	s_append(tok, "!");
+	s_nappend(tok, s_to_c(token), p - s_to_c(token) - 1);
+
+	return tok;
+}
+
+/*  Loop through the entries in a translation file looking for a match.
+ *  Return 0 if found, -1 otherwise.
+ */
+#define compare(a, b) cistrcmp(s_to_c(a), b)
+
+static int
+lookup(String **namev, String *file, String *alias)
+{
+	char *name;
+	int i, rv;
+	String *line, *token, *bangtoken;
+	Sinstack *sp;
+
+	dprint("lookup(%s, %s, %s, %s)\n", s_to_c(namev[0]), s_to_c(namev[1]),
+		s_to_c(file), s_to_c(alias));
+
+	rv = -1;
+	name = s_to_c(namev[0]);
+	line = s_new();
+	token = s_new();
+	s_reset(alias);
+	if ((sp = s_allocinstack(s_to_c(file))) == 0)
+		return -1;
+
+	/* look for a match */
+	while (s_rdinstack(sp, s_restart(line))!=0) {
+		dprint("line is %s\n", s_to_c(line));
+		s_restart(token);
+		if (s_parse(s_restart(line), token)==0)
+			continue;
+		if (compare(token, "#include")==0){
+			if(s_parse(line, s_restart(token))!=0) {
+				if(lookup(namev, line, alias) == 0)
+					break;
+			}
+			continue;
+		}
+		if (compare(token, name)!=0)
+			continue;
+		/* match found, get the alias */
+		while(s_parse(line, s_restart(token))!=0) {
+			bangtoken = attobang(token);
+
+			/* avoid definition loops */
+			for(i = 0; namev[i]; i++)
+				if(compare(bangtoken, s_to_c(namev[i]))==0) {
+					s_append(alias, "local");
+					s_append(alias, "!");
+					s_append(alias, name);
+					break;
+				}
+
+			if(namev[i] == 0)
+				s_append(alias, s_to_c(token));
+			s_append(alias, "\n");
+
+			if(bangtoken != token)
+				s_free(bangtoken);
+		}
+		rv = 0;
+		break;
+	}
+	s_free(line);
+	s_free(token);
+	s_freeinstack(sp);
+	return rv;
+}
+
+static char*
+mklower(char *name)
+{
+	char c, *p;
+
+	for(p = name; c = *p; p++)
+		if(c >= 'A' && c <= 'Z')
+			*p = c + 0x20;
+	return name;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/alias/mkfile
@@ -1,0 +1,12 @@
+</$objtype/mkfile
+
+TARG=aliasmail
+LIB=../common/libcommon.a$O
+OFILES=aliasmail.$O
+HFILES=\
+	../common/common.h\
+	../common/sys.h\
+
+</sys/src/cmd/mkone
+<../mkupas
+CFLAGS=$CFLAGS -I../common
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/addhash.c
@@ -1,0 +1,66 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "regexp.h"
+#include "hash.h"
+
+Hash hash;
+
+void
+usage(void)
+{
+	fprint(2, "addhash [-o out] file scale [file scale]...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, fd, n;
+	char err[ERRMAX], *out;
+	Biobuf *b, bout;
+
+	out = nil;
+	ARGBEGIN{
+	case 'o':
+		out = EARGF(usage());
+		break;
+	default:
+		usage();
+	}ARGEND;
+
+	if(argc==0 || argc%2)
+		usage();
+
+	while(argc > 0){
+		if((b = Bopenlock(argv[0], OREAD)) == nil)
+			sysfatal("open %s: %r", argv[0]);
+		n = atoi(argv[1]);
+		if(n == 0)
+			sysfatal("0 scale given");
+		Breadhash(b, &hash, n);
+		Bterm(b);
+		argv += 2;
+		argc -= 2;
+	}
+
+	fd = 1;
+	if(out){
+		for(i=0; i<120; i++){
+			if((fd = create(out, OWRITE, 0666|DMEXCL)) >= 0)
+				break;
+			rerrstr(err, sizeof err);
+			if(strstr(err, "file is locked")==nil && strstr(err, "exclusive lock")==nil)
+				break;
+			sleep(1000);
+		}
+		if(fd < 0)
+			sysfatal("could not open %s: %r", out);
+	}
+		
+	Binit(&bout, fd, OWRITE);
+	Bwritehash(&bout, &hash);
+	Bterm(&bout);
+	exits(0);
+}
+
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/bayes.c
@@ -1,0 +1,232 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "regexp.h"
+#include "hash.h"
+
+enum
+{
+	MAXTAB = 256,
+	MAXBEST = 32,
+};
+
+typedef struct Table Table;
+struct Table
+{
+	char *file;
+	Hash *hash;
+	int nmsg;
+};
+
+typedef struct Word Word;
+struct Word
+{
+	Stringtab *s;	/* from hmsg */
+	int count[MAXTAB];	/* counts from each table */
+	double p[MAXTAB];	/* probabilities from each table */
+	double mp;	/* max probability */
+	int mi;		/* w.p[w.mi] = w.mp */
+};
+
+Table tab[MAXTAB];
+int ntab;
+
+Word best[MAXBEST];
+int mbest;
+int nbest;
+
+int debug;
+
+void
+usage(void)
+{
+	fprint(2, "usage: bayes [-D] [-m maxword] boxhash ... ~ msghash ...\n");
+	exits("usage");
+}
+
+void*
+emalloc(int n)
+{
+	void *v;
+
+	v = mallocz(n, 1);
+	if(v == nil)
+		sysfatal("out of memory");
+	return v;
+}
+
+void
+noteword(Word *w)
+{
+	int i;
+
+	for(i=nbest-1; i>=0; i--)
+		if(w->mp < best[i].mp)
+			break;
+	i++;
+
+	if(i >= mbest)
+		return;
+	if(nbest == mbest)
+		nbest--;
+	if(i < nbest)
+		memmove(&best[i+1], &best[i], (nbest-i)*sizeof(best[0]));
+	best[i] = *w;
+	nbest++;
+}
+
+Hash*
+hread(char *s)
+{
+	Hash *h;
+	Biobuf *b;
+
+	if((b = Bopenlock(s, OREAD)) == nil)
+		sysfatal("open %s: %r", s);
+
+	h = emalloc(sizeof(Hash));
+	Breadhash(b, h, 1);
+	Bterm(b);
+	return h;
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, j, a, mi, oi, tot, keywords;
+	double totp, p, xp[MAXTAB];
+	Hash *hmsg;
+	Word w;
+	Stringtab *s, *t;
+	Biobuf bout;
+
+	mbest = 15;
+	keywords = 0;
+	ARGBEGIN{
+	case 'D':
+		debug = 1;
+		break;
+	case 'k':
+		keywords = 1;
+		break;
+	case 'm':
+		mbest = atoi(EARGF(usage()));
+		if(mbest > MAXBEST)
+			sysfatal("cannot keep more than %d words", MAXBEST);
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	for(i=0; i<argc; i++)
+		if(strcmp(argv[i], "~") == 0)
+			break;
+
+	if(i > MAXTAB)
+		sysfatal("cannot handle more than %d tables", MAXTAB);
+
+	if(i+1 >= argc)
+		usage();
+
+	for(i=0; i<argc; i++){
+		if(strcmp(argv[i], "~") == 0)
+			break;
+		tab[ntab].file = argv[i];
+		tab[ntab].hash = hread(argv[i]);
+		s = findstab(tab[ntab].hash, "*nmsg*", 6, 1);
+		if(s == nil || s->count == 0)
+			tab[ntab].nmsg = 1;
+		else
+			tab[ntab].nmsg = s->count;
+		ntab++;
+	}
+
+	Binit(&bout, 1, OWRITE);
+
+	oi = ++i;
+	for(a=i; a<argc; a++){
+		hmsg = hread(argv[a]);
+		nbest = 0;
+		for(s=hmsg->all; s; s=s->link){
+			w.s = s;
+			tot = 0;
+			totp = 0.0;
+			for(i=0; i<ntab; i++){
+				t = findstab(tab[i].hash, s->str, s->n, 0);
+				if(t == nil)
+					w.count[i] = 0;
+				else
+					w.count[i] = t->count;
+				tot += w.count[i];
+				p = w.count[i]/(double)tab[i].nmsg;
+				if(p >= 1.0)
+					p = 1.0;
+				w.p[i] = p;
+				totp += p;
+			}
+
+			if(tot < 5){		/* word does not appear enough; give to box 0 */
+				w.p[0] = 0.5;
+				for(i=1; i<ntab; i++)
+					w.p[i] = 0.1;
+				w.mp = 0.5;
+				w.mi = 0;
+				noteword(&w);
+				continue;
+			}
+
+			w.mp = 0.0;
+			for(i=0; i<ntab; i++){
+				p = w.p[i];
+				p /= totp;
+				if(p < 0.01)
+					p = 0.01;
+				else if(p > 0.99)
+					p = 0.99;
+				if(p > w.mp){
+					w.mp = p;
+					w.mi = i;
+				}
+				w.p[i] = p;
+			}
+			noteword(&w);
+		}
+
+		totp = 0.0;
+		for(i=0; i<ntab; i++){
+			p = 1.0;
+			for(j=0; j<nbest; j++)
+				p *= best[j].p[i];
+			xp[i] = p;
+			totp += p;
+		}
+		for(i=0; i<ntab; i++)
+			xp[i] /= totp;
+		mi = 0;
+		for(i=1; i<ntab; i++)
+			if(xp[i] > xp[mi])
+				mi = i;
+		if(oi != argc-1)
+			Bprint(&bout, "%s: ", argv[a]);
+		Bprint(&bout, "%s %f", tab[mi].file, xp[mi]);
+		if(keywords){
+			for(i=0; i<nbest; i++){
+				Bprint(&bout, " ");
+				Bwrite(&bout, best[i].s->str, best[i].s->n);
+				Bprint(&bout, " %f", best[i].p[mi]);
+			}
+		}
+		freehash(hmsg);
+		Bprint(&bout, "\n");
+		if(debug){
+			for(i=0; i<nbest; i++){
+				Bwrite(&bout, best[i].s->str, best[i].s->n);
+				Bprint(&bout, " %f", best[i].p[mi]);
+				if(best[i].p[mi] < best[i].mp)
+					Bprint(&bout, " (%f %s)", best[i].mp, tab[best[i].mi].file);
+				Bprint(&bout, "\n");
+			}
+		}
+	}
+	Bterm(&bout);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/classify.re
@@ -1,0 +1,403 @@
+# dreprog
+7 27 0 6 0 6
+0 1 1 0 0
+1 0 1 0 0
+0 0 5 0 0 32 1 33 0 65568 1 65569 0
+0 0 5 0 0 109 2 110 0 65645 2 65646 0
+0 0 5 0 0 111 3 112 0 65647 3 65648 0
+0 0 5 0 0 114 4 115 0 65650 4 65651 0
+0 0 5 0 0 70 5 71 0 65606 5 65607 0
+# dreprog
+5 74 0 0 0 0
+0 0 20 0 2 33 1 34 2 39 1 40 2 48 4 58 2 65 1 123 2 161 1 65536 2 65569 1 65570 2 65575 1 65576 2 65584 4 65594 2 65601 1 65659 2 65697 1
+1 0 20 0 2 33 1 34 2 39 1 40 2 48 4 58 2 65 1 123 2 161 1 65536 2 65569 1 65570 2 65575 1 65576 2 65584 4 65594 2 65601 1 65659 2 65697 1
+0 1 1 0 2
+0 0 5 0 2 48 4 58 2 65584 4 65594 2
+1 0 28 0 2 33 1 34 2 39 1 40 2 44 3 45 2 46 3 47 2 48 4 58 2 65 1 123 2 161 1 65536 2 65569 1 65570 2 65575 1 65576 2 65580 3 65581 2 65582 3 65583 2 65584 4 65594 2 65601 1 65659 2 65697 1
+# dreprog
+385 9817 0 319 0 319
+0 0 41 0 1 60 320 61 1 66 321 67 1 69 322 70 323 71 1 78 324 79 1 83 325 84 1 98 321 99 1 101 322 102 323 103 1 110 324 111 1 115 325 116 1 65596 320 65597 1 65602 321 65603 1 65605 322 65606 323 65607 1 65614 324 65615 1 65619 325 65620 1 65634 321 65635 1 65637 322 65638 323 65639 1 65646 324 65647 1 65651 325 65652 1
+1 0 1 0 2
+0 1 1 0 2
+1 0 5 0 2 33 38 91 2 65569 3 65627 2
+1 0 25 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 21 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 79 48 80 39 91 40 111 54 112 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65615 15 65616 4 65627 5 65647 22 65648 5 65659 2
+1 0 25 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 349 45 45 46 349 62 38 63 349 91 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 49 65581 51 65582 49 65598 3 65599 49 65627 344
+1 0 39 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 55 66 39 69 58 70 39 91 40 97 348 98 40 101 351 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 56 65602 4 65605 62 65606 4 65627 5 65633 343 65634 5 65637 64 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 59 84 39 91 40 115 352 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 14 65620 4 65627 5 65651 342 65652 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 73 66 74 39 91 40 105 68 106 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65609 63 65610 4 65627 5 65641 24 65642 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 78 69 79 39 91 40 110 67 111 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65614 70 65615 4 65627 5 65646 65 65647 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 71 70 39 91 40 101 72 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 331 65606 4 65627 5 65637 74 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 81 70 39 91 40 101 76 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 84 65606 4 65627 5 65637 86 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 77 350 78 39 91 40 109 82 110 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65613 353 65614 4 65627 5 65645 354 65646 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 85 112 86 39 91 40 117 114 118 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65621 333 65622 4 65627 5 65653 21 65654 5 65659 2
+1 0 10 0 41 9 2 11 41 32 2 33 41 65536 16 65545 2 65547 16 65568 2 65569 16
+1 0 21 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 232 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 233 65598 2 65601 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 186 70 40 101 186 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 17 65606 5 65637 17 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 77 136 78 40 109 136 110 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65613 18 65614 5 65645 18 65646 5 65659 2
+1 0 35 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 80 66 40 69 61 70 40 97 80 98 40 101 61 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 19 65602 5 65605 78 65606 5 65633 19 65634 5 65637 78 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 78 164 79 40 110 164 111 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65614 23 65615 5 65646 23 65647 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 85 114 86 40 117 114 118 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65621 21 65622 5 65653 21 65654 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 68 215 69 40 100 215 101 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65604 85 65605 5 65636 85 65637 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 76 126 77 40 108 126 109 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65612 57 65613 5 65644 57 65645 5 65659 2
+0 0 27 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 87 62 327 63 87 79 93 80 87 111 93 112 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 87 65598 327 65599 87 65615 93 65616 87 65647 93 65648 87
+0 0 31 0 344 9 92 10 94 11 344 13 92 14 344 32 92 33 344 70 365 71 344 73 366 74 344 102 365 103 344 105 366 106 344 65545 92 65546 94 65547 344 65549 92 65550 344 65568 92 65569 344 65606 365 65607 344 65609 366 65610 344 65638 365 65639 344 65641 366 65642 344
+0 0 39 0 344 9 26 10 27 11 344 13 26 14 344 32 26 33 344 65 28 66 344 70 25 71 344 73 346 74 344 97 28 98 344 102 25 103 344 105 346 106 344 65545 26 65546 27 65547 344 65549 26 65550 344 65568 26 65569 344 65601 28 65602 344 65606 25 65607 344 65609 346 65610 344 65633 28 65634 344 65638 25 65639 344 65641 346 65642 344
+0 0 17 0 344 9 2 11 344 13 2 14 344 32 2 33 344 62 2 63 344 65545 2 65547 344 65549 2 65550 344 65568 2 65569 344 65598 2 65599 344
+0 0 23 0 29 9 199 10 202 11 29 13 199 14 29 32 199 33 29 45 157 46 29 62 204 63 29 65545 199 65546 202 65547 29 65549 199 65550 29 65568 199 65569 29 65581 157 65582 29 65598 204 65599 29
+0 0 9 0 2 85 37 86 2 117 37 118 2 65621 37 65622 2 65653 37 65654 2
+0 0 9 0 2 68 53 69 2 100 53 101 2 65604 53 65605 2 65636 53 65637 2
+0 0 9 0 32 10 2 11 32 62 1 63 32 65546 2 65547 32 65598 1 65599 32
+0 0 9 0 2 77 36 78 2 109 36 110 2 65613 36 65614 2 65645 36 65646 2
+0 0 9 0 2 76 101 77 2 108 101 109 2 65612 101 65613 2 65644 101 65645 2
+0 0 9 0 2 87 103 88 2 119 103 120 2 65623 103 65624 2 65655 103 65656 2
+0 0 9 0 2 84 104 85 2 116 104 117 2 65620 104 65621 2 65652 104 65653 2
+0 0 9 0 2 78 105 79 2 110 105 111 2 65614 105 65615 2 65646 105 65647 2
+0 0 5 0 2 33 38 91 2 65569 3 65627 2
+0 0 25 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 21 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 10 0 41 9 2 11 41 32 2 33 41 65536 16 65545 2 65547 16 65568 2 65569 16
+0 0 25 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 349 45 45 46 349 62 38 63 349 91 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 49 65581 51 65582 49 65598 3 65599 49 65627 344
+1 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 79 108 80 95 91 87 111 93 112 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65615 110 65616 106 65627 87 65647 93 65648 87
+0 0 19 0 344 9 2 11 344 13 2 14 344 32 2 33 349 62 38 63 349 91 344 65545 2 65547 344 65549 2 65550 344 65568 2 65569 49 65598 3 65599 49 65627 344
+0 0 25 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 349 45 153 46 349 62 38 63 349 91 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 49 65581 107 65582 49 65598 3 65599 49 65627 344
+0 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 79 108 80 95 91 87 111 93 112 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65615 110 65616 106 65627 87 65647 93 65648 87
+0 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 77 109 78 95 91 87 109 99 110 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65613 111 65614 106 65627 87 65645 99 65646 87
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 85 112 86 39 91 40 117 114 118 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65621 333 65622 4 65627 5 65653 21 65654 5 65659 2
+1 0 21 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 349 62 38 63 349 91 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 49 65598 3 65599 49 65627 344
+1 0 19 0 344 9 2 11 344 13 2 14 344 32 2 33 349 62 38 63 349 91 344 65545 2 65547 344 65549 2 65550 344 65568 2 65569 49 65598 3 65599 49 65627 344
+1 0 25 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 349 45 153 46 349 62 38 63 349 91 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 49 65581 107 65582 49 65598 3 65599 49 65627 344
+1 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 77 109 78 95 91 87 109 99 110 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65613 111 65614 106 65627 87 65645 99 65646 87
+1 0 5 0 53 10 2 11 53 65546 2 65547 53
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 85 114 86 40 117 114 118 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65621 21 65622 5 65653 21 65654 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 115 85 39 91 40 116 60 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 89 65621 4 65627 5 65652 90 65653 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 115 85 39 91 40 116 60 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 89 65621 4 65627 5 65652 90 65653 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 175 70 40 101 175 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 117 65606 5 65637 117 65638 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 76 118 77 39 91 40 108 88 109 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65612 332 65613 4 65627 5 65644 121 65645 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 77 350 78 39 91 40 109 82 110 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65613 353 65614 4 65627 5 65645 354 65646 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 167 70 40 101 167 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 150 65606 5 65637 150 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 87 119 88 40 119 119 120 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65623 135 65624 5 65655 135 65656 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 76 118 77 39 91 40 108 88 109 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65612 332 65613 4 65627 5 65644 121 65645 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 76 122 77 39 91 40 108 126 109 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65612 120 65613 4 65627 5 65644 57 65645 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 76 88 77 40 108 88 109 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65612 121 65613 5 65644 121 65645 5 65659 2
+1 0 21 0 2 43 40 44 2 45 123 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 125 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 76 122 77 39 91 40 108 126 109 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65612 120 65613 4 65627 5 65644 57 65645 5 65659 2
+0 0 21 0 2 43 40 44 2 45 123 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 125 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 76 126 77 40 108 126 109 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65612 57 65613 5 65644 57 65645 5 65659 2
+0 0 25 0 2 33 38 43 39 44 38 45 127 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 128 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 25 0 2 33 38 43 39 44 38 45 127 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 128 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 129 84 39 91 40 115 360 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 77 65620 4 65627 5 65651 132 65652 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 360 84 40 115 360 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 132 65620 5 65651 132 65652 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 77 133 78 39 91 40 109 136 110 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65613 334 65614 4 65627 5 65645 18 65646 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 360 84 40 115 360 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 132 65620 5 65651 132 65652 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 77 133 78 39 91 40 109 136 110 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65613 334 65614 4 65627 5 65645 18 65646 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 70 131 71 40 102 131 103 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65606 142 65607 5 65638 142 65639 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 178 84 39 91 40 115 179 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 173 65620 4 65627 5 65651 181 65652 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 87 119 88 40 119 119 120 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65623 135 65624 5 65655 135 65656 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 87 137 88 39 91 40 119 119 120 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65623 134 65624 4 65627 5 65655 135 65656 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 77 136 78 40 109 136 110 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65613 18 65614 5 65645 18 65646 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 70 140 71 39 91 40 102 131 103 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65606 138 65607 4 65627 5 65638 142 65639 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 141 85 40 116 141 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 146 65621 5 65652 146 65653 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 87 137 88 39 91 40 119 119 120 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65623 134 65624 4 65627 5 65655 135 65656 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 70 140 71 39 91 40 102 131 103 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65606 138 65607 4 65627 5 65638 142 65639 5 65659 2
+1 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 252 66 40 97 252 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 139 65602 5 65633 139 65634 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 70 131 71 40 102 131 103 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65606 142 65607 5 65638 142 65639 5 65659 2
+0 0 9 0 87 10 32 11 87 62 1 63 87 65546 32 65547 87 65598 1 65599 87
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 73 144 74 40 105 144 106 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65609 170 65610 5 65641 170 65642 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 165 70 39 91 40 101 167 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 163 65606 4 65627 5 65637 150 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 167 70 40 101 167 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 150 65606 5 65637 150 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 73 148 74 40 105 148 106 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65609 305 65610 5 65641 305 65642 5 65659 2
+0 0 35 0 87 9 92 10 94 11 87 13 92 14 87 32 92 33 87 62 327 63 87 70 365 71 87 73 366 74 87 102 365 103 87 105 366 106 87 65545 92 65546 94 65547 87 65549 92 65550 87 65568 92 65569 87 65598 327 65599 87 65606 365 65607 87 65609 366 65610 87 65638 365 65639 87 65641 366 65642 87
+0 0 27 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 87 62 327 63 87 78 147 79 87 110 147 111 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 87 65598 327 65599 87 65614 147 65615 87 65646 147 65647 87
+0 0 43 0 87 9 92 10 27 11 87 13 92 14 87 32 92 33 87 62 327 63 87 65 100 66 87 70 151 71 87 73 346 74 87 97 100 98 87 102 151 103 87 105 346 106 87 65545 92 65546 27 65547 87 65549 92 65550 87 65568 92 65569 87 65598 327 65599 87 65601 100 65602 87 65606 151 65607 87 65609 346 65610 87 65633 100 65634 87 65638 151 65639 87 65641 346 65642 87
+0 0 13 0 87 10 32 11 87 33 95 62 3 63 95 91 87 65546 32 65547 87 65569 106 65598 3 65599 106 65627 87
+0 0 23 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 344 45 29 46 344 62 2 63 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 344 65581 29 65582 344 65598 2 65599 344
+0 0 13 0 199 10 32 11 199 45 240 46 199 62 98 63 199 65546 32 65547 199 65581 240 65582 199 65598 98 65599 199
+1 0 5 0 204 45 210 46 204 65581 210 65582 204
+0 0 27 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 87 62 327 63 87 71 100 72 87 103 100 104 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 87 65598 327 65599 87 65607 100 65608 87 65639 100 65640 87
+0 0 19 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 87 62 1 63 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 87 65598 1 65599 87
+0 0 9 0 2 69 368 70 2 101 368 102 2 65605 368 65606 2 65637 368 65638 2
+0 0 9 0 2 69 154 70 2 101 154 102 2 65605 154 65606 2 65637 154 65638 2
+0 0 9 0 2 83 155 84 2 115 155 116 2 65619 155 65620 2 65651 155 65652 2
+0 0 9 0 2 80 156 81 2 112 156 113 2 65616 156 65617 2 65648 156 65649 2
+0 0 9 0 2 68 205 69 2 100 205 101 2 65604 205 65605 2 65636 205 65637 2
+1 0 13 0 87 10 32 11 87 33 95 62 3 63 95 91 87 65546 32 65547 87 65569 106 65598 3 65599 106 65627 87
+1 0 25 0 29 9 199 10 202 11 29 13 199 14 29 32 199 33 153 45 208 46 153 62 211 63 153 91 29 65545 199 65546 202 65547 29 65549 199 65550 29 65568 199 65569 107 65581 159 65582 107 65598 213 65599 107 65627 29
+0 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 78 158 79 95 91 87 110 147 111 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65614 113 65615 106 65627 87 65646 147 65647 87
+0 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 71 160 72 95 91 87 103 100 104 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65607 161 65608 106 65627 87 65639 100 65640 87
+1 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 78 158 79 95 91 87 110 147 111 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65614 113 65615 106 65627 87 65646 147 65647 87
+1 0 29 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 49 63 95 71 160 72 95 91 87 103 100 104 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65607 161 65608 106 65627 87 65639 100 65640 87
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 78 162 79 39 91 40 110 164 111 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65614 149 65615 4 65627 5 65646 23 65647 5 65659 2
+1 0 27 0 87 9 32 11 87 13 32 14 87 32 32 33 95 62 49 63 95 84 160 85 95 91 87 116 100 117 87 65545 32 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65620 161 65621 106 65627 87 65652 100 65653 87
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 78 164 79 40 110 164 111 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65614 23 65615 5 65646 23 65647 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 165 70 39 91 40 101 167 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 163 65606 4 65627 5 65637 150 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 78 220 79 39 91 40 110 221 111 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65614 217 65615 4 65627 5 65646 341 65647 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 78 221 79 40 110 221 111 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65614 341 65615 5 65646 341 65647 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 73 168 74 39 91 40 105 144 106 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65609 166 65610 4 65627 5 65641 170 65642 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 190 84 40 115 190 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 185 65620 5 65651 185 65652 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 171 70 39 91 40 101 175 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 116 65606 4 65627 5 65637 117 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 73 144 74 40 105 144 106 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65609 170 65610 5 65641 170 65642 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 171 70 39 91 40 101 175 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 116 65606 4 65627 5 65637 117 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 172 83 40 114 172 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 174 65619 5 65650 174 65651 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 73 302 74 39 91 40 105 148 106 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65609 303 65610 4 65627 5 65641 305 65642 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 172 83 40 114 172 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 174 65619 5 65650 174 65651 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 175 70 40 101 175 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 117 65606 5 65637 117 65638 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 176 83 39 91 40 114 172 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 145 65619 4 65627 5 65650 174 65651 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 176 83 39 91 40 114 172 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 145 65619 4 65627 5 65650 174 65651 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 178 84 39 91 40 115 179 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 173 65620 4 65627 5 65651 181 65652 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 80 194 81 39 91 40 112 198 113 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65616 196 65617 4 65627 5 65648 193 65649 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 383 70 40 101 383 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 384 65606 5 65637 384 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 179 84 40 115 179 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 181 65620 5 65651 181 65652 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 182 70 39 91 40 101 186 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 184 65606 4 65627 5 65637 17 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 187 84 39 91 40 115 190 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 188 65620 4 65627 5 65651 185 65652 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 190 84 40 115 190 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 185 65620 5 65651 185 65652 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 186 70 40 101 186 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 17 65606 5 65637 17 65638 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 187 84 39 91 40 115 190 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 188 65620 4 65627 5 65651 185 65652 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 191 70 39 91 40 101 383 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 192 65606 4 65627 5 65637 384 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 268 83 40 114 268 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 189 65619 5 65650 189 65651 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 191 70 39 91 40 101 383 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 192 65606 4 65627 5 65637 384 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 80 198 81 40 112 198 113 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65616 193 65617 5 65648 193 65649 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 383 70 40 101 383 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 384 65606 5 65637 384 65638 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 80 194 81 39 91 40 112 198 113 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65616 196 65617 4 65627 5 65648 193 65649 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 86 372 87 40 118 372 119 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65622 374 65623 5 65654 374 65655 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 222 70 39 91 40 101 183 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 223 65606 4 65627 5 65637 201 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 80 198 81 40 112 198 113 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65616 193 65617 5 65648 193 65649 5 65659 2
+0 0 25 0 87 9 32 11 87 13 32 14 87 32 32 33 87 62 327 63 87 84 100 85 87 116 100 117 87 65545 32 65547 87 65549 32 65550 87 65568 32 65569 87 65598 327 65599 87 65620 100 65621 87 65652 100 65653 87
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 68 167 69 40 100 167 101 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65604 150 65605 5 65636 150 65637 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 68 212 69 39 91 40 100 215 101 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65604 200 65605 4 65627 5 65636 85 65637 5 65659 2
+1 0 23 0 2 43 40 44 2 45 40 46 2 47 40 58 218 59 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 218 65595 2 65597 5 65598 2 65601 5 65659 2
+0 0 27 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 87 62 327 63 87 79 93 80 87 111 93 112 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 87 65598 327 65599 87 65615 93 65616 87 65647 93 65648 87
+0 0 13 0 202 10 2 11 202 45 242 46 202 62 98 63 202 65546 2 65547 202 65581 242 65582 202 65598 98 65599 202
+0 0 25 0 29 9 199 10 202 11 29 13 199 14 29 32 199 33 153 45 208 46 153 62 211 63 153 91 29 65545 199 65546 202 65547 29 65549 199 65550 29 65568 199 65569 107 65581 159 65582 107 65598 213 65599 107 65627 29
+0 0 5 0 2 61 203 62 2 65597 203 65598 2
+0 0 5 0 2 58 207 59 2 65594 207 65595 2
+0 0 5 0 2 32 209 33 2 65568 209 65569 2
+0 0 23 0 29 9 199 10 32 11 29 13 199 14 29 32 199 33 29 45 238 46 29 62 204 63 29 65545 199 65546 32 65547 29 65549 199 65550 29 65568 199 65569 29 65581 238 65582 29 65598 204 65599 29
+0 0 27 0 87 9 32 11 87 13 32 14 87 32 32 33 95 62 49 63 95 84 160 85 95 91 87 116 100 117 87 65545 32 65547 87 65549 32 65550 87 65568 32 65569 106 65598 49 65599 106 65620 161 65621 106 65627 87 65652 100 65653 87
+1 0 25 0 29 9 199 10 32 11 29 13 199 14 29 32 199 33 153 45 243 46 153 62 211 63 153 91 29 65545 199 65546 32 65547 29 65549 199 65550 29 65568 199 65569 107 65581 235 65582 107 65598 213 65599 107 65627 29
+0 0 21 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 3 63 95 91 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 3 65599 106 65627 87
+1 0 21 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 95 62 3 63 95 91 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 106 65598 3 65599 106 65627 87
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 68 212 69 39 91 40 100 215 101 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65604 200 65605 4 65627 5 65636 85 65637 5 65659 2
+1 0 27 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 216 59 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 169 65595 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 68 215 69 40 100 215 101 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65604 85 65605 5 65636 85 65637 5 65659 2
+0 0 27 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 216 59 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 169 65595 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 86 371 87 39 91 40 118 372 119 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65622 373 65623 4 65627 5 65654 374 65655 5 65659 2
+0 0 23 0 2 43 40 44 2 45 40 46 2 47 40 58 218 59 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 218 65595 2 65597 5 65598 2 65601 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 86 371 87 39 91 40 118 372 119 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65622 373 65623 4 65627 5 65654 374 65655 5 65659 2
+1 0 9 0 218 10 241 11 218 33 216 91 218 65546 241 65547 218 65569 169 65627 218
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 86 372 87 40 118 372 119 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65622 374 65623 5 65654 374 65655 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 78 220 79 39 91 40 110 221 111 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65614 217 65615 4 65627 5 65646 341 65647 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 183 70 40 101 183 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 201 65606 5 65637 201 65638 5 65659 2
+1 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 224 66 39 91 40 97 225 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 226 65602 4 65627 5 65633 227 65634 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 183 70 40 101 183 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 201 65606 5 65637 201 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 78 221 79 40 110 221 111 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65614 341 65615 5 65646 341 65647 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 222 70 39 91 40 101 183 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 223 65606 4 65627 5 65637 201 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 78 283 79 39 91 40 110 280 111 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65614 281 65615 4 65627 5 65646 282 65647 5 65659 2
+0 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 224 66 39 91 40 97 225 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 226 65602 4 65627 5 65633 227 65634 5 65659 2
+0 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 225 66 40 97 225 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 227 65602 5 65633 227 65634 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 310 70 39 91 40 101 369 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 312 65606 4 65627 5 65637 370 65638 5 65659 2
+1 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 225 66 40 97 225 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 227 65602 5 65633 227 65634 5 65659 2
+0 0 25 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 228 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 219 65598 3 65601 4 65627 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 80 229 81 40 112 229 113 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65616 355 65617 5 65648 355 65649 5 65659 2
+1 0 25 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 228 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 219 65598 3 65601 4 65627 5 65659 2
+1 0 23 0 2 43 40 44 2 45 40 46 2 47 40 58 207 59 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 207 65595 2 65597 5 65598 2 65601 5 65659 2
+0 0 21 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 232 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 233 65598 2 65601 5 65659 2
+0 0 27 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 234 59 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 214 65595 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 27 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 234 59 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 214 65595 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 89 290 90 40 121 290 122 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65625 292 65626 5 65657 292 65658 5 65659 2
+0 0 23 0 2 43 40 44 2 45 40 46 2 47 40 58 207 59 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 207 65595 2 65597 5 65598 2 65601 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 236 83 39 91 40 114 377 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 230 65619 4 65627 5 65650 379 65651 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 236 83 39 91 40 114 377 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 230 65619 4 65627 5 65650 379 65651 5 65659 2
+1 0 25 0 2 32 209 33 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65568 209 65569 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 27 0 2 32 209 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65568 209 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 262 83 40 114 262 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 263 65619 5 65650 263 65651 5 65659 2
+1 0 27 0 2 32 209 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65568 209 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 262 83 40 114 262 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 263 65619 5 65650 263 65651 5 65659 2
+0 0 25 0 2 32 209 33 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65568 209 65569 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 13 0 199 10 202 11 199 45 97 46 199 62 98 63 199 65546 202 65547 199 65581 97 65582 199 65598 98 65599 199
+1 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 250 66 39 91 40 97 252 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 239 65602 4 65627 5 65633 139 65634 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 80 229 81 40 112 229 113 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65616 355 65617 5 65648 355 65649 5 65659 2
+0 0 13 0 202 10 204 11 202 45 152 46 202 62 98 63 202 65546 204 65547 202 65581 152 65582 202 65598 98 65599 202
+0 0 5 0 2 34 1 35 2 65570 1 65571 2
+0 0 5 0 204 45 210 46 204 65581 210 65582 204
+0 0 9 0 2 65 244 66 2 97 244 98 2 65601 244 65602 2 65633 244 65634 2
+0 0 9 0 2 65 347 66 2 97 347 98 2 65601 347 65602 2 65633 347 65634 2
+0 0 5 0 2 60 245 61 2 65596 245 65597 2
+0 0 25 0 29 9 199 10 32 11 29 13 199 14 29 32 199 33 153 45 243 46 153 62 211 63 153 91 29 65545 199 65546 32 65547 29 65549 199 65550 29 65568 199 65569 107 65581 235 65582 107 65598 213 65599 107 65627 29
+0 0 9 0 2 73 246 74 2 105 246 106 2 65609 246 65610 2 65641 246 65642 2
+0 0 9 0 204 10 2 11 204 45 261 46 204 65546 2 65547 204 65581 261 65582 204
+0 0 9 0 204 33 211 45 248 46 211 91 204 65569 213 65581 237 65582 213 65627 204
+0 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 250 66 39 91 40 97 252 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 239 65602 4 65627 5 65633 139 65634 5 65659 2
+1 0 9 0 204 33 211 45 248 46 211 91 204 65569 213 65581 237 65582 213 65627 204
+1 0 9 0 2 33 38 60 257 61 38 91 2 65569 3 65596 251 65597 3 65627 2
+0 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 252 66 40 97 252 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 139 65602 5 65633 139 65634 5 65659 2
+0 0 9 0 218 10 241 11 218 33 216 91 218 65546 241 65547 218 65569 169 65627 218
+1 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 75 66 39 91 40 97 80 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 73 65602 4 65627 5 65633 19 65634 5 65659 2
+0 0 5 0 218 10 241 11 218 65546 241 65547 218
+1 0 27 0 2 33 38 34 3 35 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 75 66 39 91 40 97 80 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 73 65602 4 65627 5 65633 19 65634 5 65659 2
+0 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 80 66 40 97 80 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 19 65602 5 65633 19 65634 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 80 255 81 39 91 40 112 229 113 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65616 256 65617 4 65627 5 65648 355 65649 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 80 255 81 39 91 40 112 229 113 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65616 256 65617 4 65627 5 65648 355 65649 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 71 376 72 39 91 40 103 380 104 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65607 378 65608 4 65627 5 65639 381 65640 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 71 380 72 40 103 380 104 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65607 381 65608 5 65639 381 65640 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 71 376 72 39 91 40 103 380 104 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65607 378 65608 4 65627 5 65639 381 65640 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 71 380 72 40 103 380 104 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65607 381 65608 5 65639 381 65640 5 65659 2
+0 0 27 0 2 33 38 34 3 35 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 76 272 77 40 108 272 109 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65612 274 65613 5 65644 274 65645 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 260 70 39 91 40 101 258 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 177 65606 4 65627 5 65637 231 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 78 280 79 40 110 280 111 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65614 282 65615 5 65646 282 65647 5 65659 2
+0 0 25 0 2 34 1 35 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65570 1 65571 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+1 0 25 0 2 34 1 35 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65570 1 65571 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 9 0 2 33 38 60 257 61 38 91 2 65569 3 65596 251 65597 3 65627 2
+1 0 21 0 29 9 199 10 32 11 29 13 199 14 29 32 199 33 153 62 3 63 153 91 29 65545 199 65546 32 65547 29 65549 199 65550 29 65568 199 65569 107 65598 3 65599 107 65627 29
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 260 70 39 91 40 101 258 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 177 65606 4 65627 5 65637 231 65638 5 65659 2
+1 0 13 0 204 10 2 11 204 33 211 45 265 46 211 91 204 65546 2 65547 204 65569 213 65581 249 65582 213 65627 204
+0 0 19 0 29 9 199 10 32 11 29 13 199 14 29 32 199 33 29 62 1 63 29 65545 199 65546 32 65547 29 65549 199 65550 29 65568 199 65569 29 65598 1 65599 29
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 266 83 39 91 40 114 268 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 259 65619 4 65627 5 65650 189 65651 5 65659 2
+0 0 9 0 199 10 32 11 199 62 1 63 199 65546 32 65547 199 65598 1 65599 199
+1 0 9 0 2 9 218 10 2 32 218 33 2 65545 218 65546 2 65568 218 65569 2
+0 0 9 0 202 10 2 11 202 62 1 63 202 65546 2 65547 202 65598 1 65599 202
+0 0 21 0 29 9 199 10 32 11 29 13 199 14 29 32 199 33 153 62 3 63 153 91 29 65545 199 65546 32 65547 29 65549 199 65550 29 65568 199 65569 107 65598 3 65599 107 65627 29
+0 0 9 0 2 82 247 83 2 114 247 115 2 65618 247 65619 2 65650 247 65651 2
+0 0 9 0 32 10 2 11 32 62 2 63 32 65546 2 65547 32 65598 2 65599 32
+0 0 9 0 2 68 264 69 2 100 264 101 2 65604 264 65605 2 65636 264 65637 2
+0 0 9 0 2 89 285 90 2 121 285 122 2 65625 285 65626 2 65657 285 65658 2
+0 0 13 0 204 10 2 11 204 33 211 45 265 46 211 91 204 65546 2 65547 204 65569 213 65581 249 65582 213 65627 204
+1 0 13 0 204 10 2 11 204 33 211 62 3 63 211 91 204 65546 2 65547 204 65569 213 65598 3 65599 213 65627 204
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 266 83 39 91 40 114 268 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 259 65619 4 65627 5 65650 189 65651 5 65659 2
+1 0 13 0 32 10 2 11 32 33 279 62 38 63 279 91 32 65546 2 65547 32 65569 267 65598 3 65599 267 65627 32
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 268 83 40 114 268 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 189 65619 5 65650 189 65651 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 269 83 39 91 40 114 262 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 270 65619 4 65627 5 65650 263 65651 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 82 269 83 39 91 40 114 262 115 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65618 270 65619 4 65627 5 65650 263 65651 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 76 271 77 39 91 40 108 272 109 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65612 273 65613 4 65627 5 65644 274 65645 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 76 271 77 39 91 40 108 272 109 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65612 273 65613 4 65627 5 65644 274 65645 5 65659 2
+0 0 13 0 32 10 2 11 32 33 279 62 38 63 279 91 32 65546 2 65547 32 65569 267 65598 3 65599 267 65627 32
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 78 280 79 40 110 280 111 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65614 282 65615 5 65646 282 65647 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 89 289 90 39 91 40 121 290 122 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65625 284 65626 4 65627 5 65657 292 65658 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 78 283 79 39 91 40 110 280 111 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65614 281 65615 4 65627 5 65646 282 65647 5 65659 2
+0 0 9 0 204 10 2 11 204 62 1 63 204 65546 2 65547 204 65598 1 65599 204
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 89 357 90 40 121 357 122 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65625 359 65626 5 65657 359 65658 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 89 357 90 40 121 357 122 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65625 359 65626 5 65657 359 65658 5 65659 2
+0 0 5 0 2 32 53 33 2 65568 53 65569 2
+0 0 13 0 204 10 2 11 204 33 211 62 3 63 211 91 204 65546 2 65547 204 65569 213 65598 3 65599 213 65627 204
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 89 289 90 39 91 40 121 290 122 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65625 284 65626 4 65627 5 65657 292 65658 5 65659 2
+1 0 13 0 32 10 2 11 32 33 279 62 3 63 279 91 32 65546 2 65547 32 65569 267 65598 3 65599 267 65627 32
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 89 290 90 40 121 290 122 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65625 292 65626 5 65657 292 65658 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 89 356 90 39 91 40 121 357 122 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65625 358 65626 4 65627 5 65657 359 65658 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 89 356 90 39 91 40 121 357 122 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65625 358 65626 4 65627 5 65657 359 65658 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 89 291 90 39 91 40 121 286 122 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65625 287 65626 4 65627 5 65657 288 65658 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 89 286 90 40 121 286 122 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65625 288 65626 5 65657 288 65658 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 89 291 90 39 91 40 121 286 122 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65625 287 65626 4 65627 5 65657 288 65658 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 89 286 90 40 121 286 122 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65625 288 65626 5 65657 288 65658 5 65659 2
+0 0 25 0 2 33 38 43 39 44 38 45 293 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 124 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 21 0 2 43 40 44 2 45 91 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 296 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+1 0 25 0 2 33 38 43 39 44 38 45 293 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 124 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 21 0 2 43 40 44 2 45 91 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 296 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 13 0 32 10 2 11 32 33 279 62 3 63 279 91 32 65546 2 65547 32 65569 267 65598 3 65599 267 65627 32
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 67 298 68 40 99 298 100 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65603 299 65604 5 65635 299 65636 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 67 297 68 39 91 40 99 298 100 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65603 180 65604 4 65627 5 65635 299 65636 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 67 298 68 40 99 298 100 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65603 299 65604 5 65635 299 65636 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 67 297 68 39 91 40 99 298 100 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65603 180 65604 4 65627 5 65635 299 65636 5 65659 2
+1 0 25 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 53 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 53 65598 3 65601 4 65627 5 65659 2
+0 0 5 0 2 61 53 62 2 65597 53 65598 2
+0 0 21 0 2 43 40 44 2 45 362 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 364 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+1 0 25 0 2 33 38 43 39 44 38 45 361 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 363 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 21 0 2 43 40 44 2 45 362 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 364 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 25 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 53 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 53 65598 3 65601 4 65627 5 65659 2
+0 0 21 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 53 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 53 65598 2 65601 5 65659 2
+0 0 25 0 2 33 38 43 39 44 38 45 361 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 363 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 21 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 53 62 2 65 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 53 65598 2 65601 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 73 302 74 39 91 40 105 148 106 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65609 303 65610 4 65627 5 65641 305 65642 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 68 304 69 40 100 304 101 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65604 313 65605 5 65636 313 65637 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 68 314 69 39 91 40 100 304 101 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65604 315 65605 4 65627 5 65636 313 65637 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 73 148 74 40 105 148 106 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65609 305 65610 5 65641 305 65642 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 310 70 39 91 40 101 369 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 312 65606 4 65627 5 65637 370 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 369 70 40 101 369 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 370 65606 5 65637 370 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 369 70 40 101 369 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 370 65606 5 65637 370 65638 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 68 314 69 39 91 40 100 304 101 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65604 315 65605 4 65627 5 65636 313 65637 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 68 304 69 40 100 304 101 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65604 313 65605 5 65636 313 65637 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 68 165 69 39 91 40 100 167 101 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65604 163 65605 4 65627 5 65636 150 65637 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 68 165 69 39 91 40 100 167 101 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65604 163 65605 4 65627 5 65636 150 65637 5 65659 2
+0 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 308 66 40 97 308 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 309 65602 5 65633 309 65634 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 68 167 69 40 100 167 101 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65604 150 65605 5 65636 150 65637 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 115 85 39 91 40 116 60 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 89 65621 4 65627 5 65652 90 65653 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 115 85 39 91 40 116 60 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 89 65621 4 65627 5 65652 90 65653 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 60 85 40 116 60 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 90 65621 5 65652 90 65653 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 60 85 40 116 60 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 90 65621 5 65652 90 65653 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 165 84 39 91 40 115 167 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 163 65620 4 65627 5 65651 150 65652 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 79 167 80 40 111 167 112 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65615 150 65616 5 65647 150 65648 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 165 84 39 91 40 115 167 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 163 65620 4 65627 5 65651 150 65652 5 65659 2
+1 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 308 66 40 97 308 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 309 65602 5 65633 309 65634 5 65659 2
+0 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 306 66 39 91 40 97 308 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 307 65602 4 65627 5 65633 309 65634 5 65659 2
+1 0 31 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 306 66 39 91 40 97 308 98 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 307 65602 4 65627 5 65633 309 65634 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 79 165 80 39 91 40 111 167 112 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65615 163 65616 4 65627 5 65647 150 65648 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 79 165 80 39 91 40 111 167 112 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65615 163 65616 4 65627 5 65647 150 65648 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 79 167 80 40 111 167 112 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65615 150 65616 5 65647 150 65648 5 65659 2
+0 0 87 0 1 9 326 10 1 33 3 43 4 44 3 45 329 46 3 47 4 58 3 60 328 61 4 62 3 65 4 66 6 67 4 68 8 69 9 70 10 71 4 73 11 74 4 77 12 78 330 79 4 82 13 83 14 84 4 91 5 98 335 99 5 100 336 101 382 102 337 103 5 105 338 106 5 109 339 110 20 111 5 114 340 115 342 116 5 123 1 65545 326 65546 1 65569 3 65579 4 65580 3 65581 329 65582 3 65583 4 65594 3 65596 328 65597 4 65598 3 65601 4 65602 6 65603 4 65604 8 65605 9 65606 10 65607 4 65609 11 65610 4 65613 12 65614 330 65615 4 65618 13 65619 14 65620 4 65627 5 65634 335 65635 5 65636 336 65637 382 65638 337 65639 5 65641 338 65642 5 65645 339 65646 20 65647 5 65650 340 65651 342 65652 5 65659 1
+1 0 41 0 344 9 26 10 27 11 344 13 26 14 344 32 26 33 345 34 344 65 28 66 344 70 25 71 344 73 346 74 344 97 28 98 344 102 25 103 344 105 346 106 344 65545 26 65546 27 65547 344 65549 26 65550 344 65568 26 65569 345 65570 344 65601 28 65602 344 65606 25 65607 344 65609 346 65610 344 65633 28 65634 344 65638 25 65639 344 65641 346 65642 344
+1 0 9 0 2 79 30 80 2 111 30 112 2 65615 30 65616 2 65647 30 65648 2
+1 0 9 0 2 83 33 84 2 115 33 116 2 65619 33 65620 2 65651 33 65652 2
+1 0 9 0 2 73 34 74 2 105 34 106 2 65609 34 65610 2 65641 34 65642 2
+1 0 17 0 2 65 347 66 2 69 35 70 2 97 347 98 2 101 35 102 2 65601 347 65602 2 65605 35 65606 2 65633 347 65634 2 65637 35 65638 2
+1 0 9 0 2 77 36 78 2 109 36 110 2 65613 36 65614 2 65645 36 65646 2
+1 0 9 0 2 73 31 74 2 105 31 106 2 65609 31 65610 2 65641 31 65642 2
+1 0 19 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 344 62 2 63 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 344 65598 2 65599 344
+1 0 43 0 344 9 26 10 27 11 344 13 26 14 344 32 26 33 42 34 349 65 44 66 349 70 46 71 349 73 47 74 349 91 344 97 28 98 344 102 25 103 344 105 346 106 344 65545 26 65546 27 65547 344 65549 26 65550 344 65568 26 65569 7 65570 49 65601 50 65602 49 65606 43 65607 49 65609 52 65610 49 65627 344 65633 28 65634 344 65638 25 65639 344 65641 346 65642 344
+1 0 25 0 2 33 38 43 39 44 38 45 41 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 16 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 39 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 75 66 39 69 79 70 39 91 40 97 80 98 40 101 61 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 73 65602 4 65605 83 65606 4 65627 5 65633 19 65634 5 65637 78 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 83 129 84 39 91 40 115 360 116 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65619 77 65620 4 65627 5 65651 132 65652 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 73 168 74 39 91 40 105 144 106 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65609 166 65610 4 65627 5 65641 170 65642 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 78 162 79 39 91 40 110 164 111 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65614 149 65615 4 65627 5 65646 23 65647 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 182 70 39 91 40 101 186 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 184 65606 4 65627 5 65637 17 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 79 54 80 40 111 54 112 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65615 22 65616 5 65647 22 65648 5 65659 2
+1 0 35 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 348 66 40 69 351 70 40 97 348 98 40 101 351 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 343 65602 5 65605 64 65606 5 65633 343 65634 5 65637 64 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 73 68 74 40 105 68 106 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65609 24 65610 5 65641 24 65642 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 78 67 79 40 110 67 111 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65614 65 65615 5 65646 65 65647 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 72 70 40 101 72 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 74 65606 5 65637 74 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 76 70 40 101 76 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 86 65606 5 65637 86 65638 5 65659 2
+1 0 27 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 80 66 40 97 80 98 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 19 65602 5 65633 19 65634 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 77 82 78 40 109 82 110 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65613 354 65614 5 65645 354 65646 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 60 85 40 116 60 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 90 65621 5 65652 90 65653 5 65659 2
+0 0 19 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 344 62 2 63 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 344 65598 2 65599 344
+0 0 23 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 344 45 96 46 344 62 2 63 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 344 65581 96 65582 344 65598 2 65599 344
+0 0 27 0 87 9 32 10 2 11 87 13 32 14 87 32 32 33 87 62 327 63 87 77 99 78 87 109 99 110 87 65545 32 65546 2 65547 87 65549 32 65550 87 65568 32 65569 87 65598 327 65599 87 65613 99 65614 87 65645 99 65646 87
+0 0 9 0 2 77 102 78 2 109 102 110 2 65613 102 65614 2 65645 102 65646 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 60 85 40 116 60 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 90 65621 5 65652 90 65653 5 65659 2
+0 0 21 0 344 9 87 10 32 11 344 13 87 14 344 32 87 33 349 62 38 63 349 91 344 65545 87 65546 32 65547 344 65549 87 65550 344 65568 87 65569 49 65598 3 65599 49 65627 344
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 143 85 39 91 40 116 141 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 130 65621 4 65627 5 65652 146 65653 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 76 88 77 40 108 88 109 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65612 121 65613 5 65644 121 65645 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 77 82 78 40 109 82 110 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65613 354 65614 5 65645 354 65646 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 143 85 39 91 40 116 141 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 130 65621 4 65627 5 65652 146 65653 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 141 85 40 116 141 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 146 65621 5 65652 146 65653 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 76 272 77 40 108 272 109 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65612 274 65613 5 65644 274 65645 5 65659 2
+0 0 25 0 2 33 38 43 39 44 38 45 300 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 295 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+0 0 21 0 2 43 40 44 2 45 294 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 301 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+1 0 25 0 2 33 38 43 39 44 38 45 300 46 38 47 39 58 38 61 39 62 38 65 39 91 40 123 2 65569 3 65579 4 65580 3 65581 295 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65627 5 65659 2
+1 0 21 0 2 43 40 44 2 45 294 46 2 47 40 58 2 61 40 62 2 65 40 123 2 65579 5 65580 2 65581 301 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 179 84 40 115 179 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 181 65620 5 65651 181 65652 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 316 85 39 91 40 116 311 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 317 65621 4 65627 5 65652 318 65653 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 311 85 40 116 311 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 318 65621 5 65652 318 65653 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 84 316 85 39 91 40 116 311 117 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65620 317 65621 4 65627 5 65652 318 65653 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 84 311 85 40 116 311 117 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65620 318 65621 5 65652 318 65653 5 65659 2
+0 0 17 0 87 10 32 11 87 62 327 63 87 79 367 80 87 111 367 112 87 65546 32 65547 87 65598 327 65599 87 65615 367 65616 87 65647 367 65648 87
+0 0 17 0 87 10 32 11 87 62 327 63 87 77 375 78 87 109 375 110 87 65546 32 65547 87 65598 327 65599 87 65613 375 65614 87 65645 375 65646 87
+0 0 17 0 87 10 32 11 87 62 327 63 87 78 375 79 87 110 375 111 87 65546 32 65547 87 65598 327 65599 87 65614 375 65615 87 65646 375 65647 87
+0 0 9 0 2 78 206 79 2 110 206 111 2 65614 206 65615 2 65646 206 65647 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 167 84 40 115 167 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 150 65620 5 65651 150 65652 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 167 84 40 115 167 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 150 65620 5 65651 150 65652 5 65659 2
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 253 70 39 91 40 101 195 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 254 65606 4 65627 5 65637 197 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 195 70 40 101 195 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 197 65606 5 65637 197 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 253 70 39 91 40 101 195 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 254 65606 4 65627 5 65637 197 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 195 70 40 101 195 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 197 65606 5 65637 197 65638 5 65659 2
+0 0 9 0 87 10 32 11 87 62 327 63 87 65546 32 65547 87 65598 327 65599 87
+0 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 275 70 39 91 40 101 276 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 277 65606 4 65627 5 65637 278 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 258 70 40 101 258 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 231 65606 5 65637 231 65638 5 65659 2
+1 0 33 0 2 33 38 43 39 44 38 45 39 46 38 47 39 58 38 61 39 62 38 65 39 69 275 70 39 91 40 101 276 102 40 123 2 65569 3 65579 4 65580 3 65581 4 65582 3 65583 4 65594 3 65597 4 65598 3 65601 4 65605 277 65606 4 65627 5 65637 278 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 258 70 40 101 258 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 231 65606 5 65637 231 65638 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 276 70 40 101 276 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 278 65606 5 65637 278 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 69 276 70 40 101 276 102 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65605 278 65606 5 65637 278 65638 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 83 352 84 40 115 352 116 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65619 342 65620 5 65651 342 65652 5 65659 2
+0 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 377 83 40 114 377 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 379 65619 5 65650 379 65651 5 65659 2
+1 0 29 0 2 43 40 44 2 45 40 46 2 47 40 58 2 61 40 62 2 65 40 82 377 83 40 114 377 115 40 123 2 65579 5 65580 2 65581 5 65582 2 65583 5 65594 2 65597 5 65598 2 65601 5 65618 379 65619 5 65650 379 65651 5 65659 2
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/dfa.c
@@ -1,0 +1,800 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <bio.h>
+#include "regexp.h"
+#include "regcomp.h"
+#include "dfa.h"
+
+void rdump(Reprog*);
+void dump(Dreprog*);
+
+/*
+ * Standard NFA determinization and DFA minimization.
+ */
+typedef struct Deter Deter;
+typedef struct Reiset Reiset;
+
+void ddump(Deter*);
+
+/* state of determinization */
+struct Deter
+{
+	jmp_buf kaboom;	/* jmp on error */
+
+	Bin *bin;		/* bin for temporary allocations */
+
+	Reprog *p;	/* program being determinized */
+	uint ninst;		/* number of instructions in program */
+
+	Reiset *alloc;	/* chain of all Reisets */
+	Reiset **last;
+
+	Reiset **hash;	/* hash of all Reisets */
+	uint nhash;
+
+	Reiset *tmp;	/* temporaries for walk */
+	uchar *bits;
+
+	Rune *c;		/* ``interesting'' characters */
+	uint nc;
+};
+
+/* set of Reinsts: perhaps we should use a bit list instead of the indices? */
+struct Reiset
+{
+	uint *inst;		/* indices of instructions in set */
+	uint ninst;		/* size of set */
+
+	Reiset *next;	/* d.alloc chain */
+	Reiset *hash;	/* d.hash chain */
+	Reiset **delta;	/* where to go on each interesting char */
+	uint id;		/* assigned id during minimization */
+	uint isfinal;	/* is an accepting (final) state */
+};
+
+static Reiset*
+ralloc(Deter *d, int ninst)
+{
+	Reiset *t;
+
+	t = binalloc(&d->bin, sizeof(Reiset)+2*d->nc*sizeof(Reiset*)+sizeof(uint)*ninst, 0);
+	if(t == nil)
+		longjmp(d->kaboom, 1);
+	t->delta = (Reiset**)&t[1];
+	t->inst = (uint*)&t->delta[2*d->nc];
+	return t;
+}
+
+/* find the canonical form a given Reiset */
+static Reiset*
+findreiset(Deter *d, Reiset *s)
+{
+	int i, szinst;
+	uint h;
+	Reiset *t;
+
+	h = 0;
+	for(i=0; i<s->ninst; i++)
+		h = h*1000003 + s->inst[i];
+	h %= d->nhash;
+
+	szinst = s->ninst*sizeof(s->inst[0]);
+	for(t=d->hash[h]; t; t=t->hash)
+		if(t->ninst==s->ninst && memcmp(t->inst, s->inst, szinst)==0)
+			return t;
+
+	t = ralloc(d, s->ninst);
+	t->hash = d->hash[h];
+	d->hash[h] = t;
+
+	*d->last = t;
+	d->last = &t->next;
+	t->next = 0;
+
+	t->ninst = s->ninst;
+	memmove(t->inst, s->inst, szinst);
+
+	/* delta is filled in later */
+
+	return t;
+}
+
+/* convert bits to a real reiset */
+static Reiset*
+bits2reiset(Deter *d, uchar *bits)
+{
+	int k;
+	Reiset *s;
+
+	s = d->tmp;
+	s->ninst = 0;
+	for(k=0; k<d->ninst; k++)
+		if(bits[k])
+			s->inst[s->ninst++] = k;
+	return findreiset(d, s);
+}
+
+/* add n to state set; if n < k, need to go around again */
+static int
+add(int n, uchar *bits, int k)
+{
+	if(bits[n])
+		return 0;
+	bits[n] = 1;
+	return n < k;
+}
+
+/* update bits to follow all the empty (non-character-related) transitions possible */
+static void
+followempty(Deter *d, uchar *bits, int bol, int eol)
+{
+	int again, k;
+	Reinst *i;
+
+	do{
+		again = 0;
+		for(i=d->p->firstinst, k=0; k < d->ninst; i++, k++){
+			if(!bits[k])
+				continue;
+			switch(i->type){
+			case RBRA:
+			case LBRA:
+				again |= add(i->next - d->p->firstinst, bits, k);
+				break;
+			case OR:
+				again |= add(i->left - d->p->firstinst, bits, k);
+				again |= add(i->right - d->p->firstinst, bits, k);
+				break;
+			case BOL:
+				if(bol)
+					again |= add(i->next - d->p->firstinst, bits, k);
+				break;
+			case EOL:
+				if(eol)
+					again |= add(i->next - d->p->firstinst, bits, k);
+				break;
+			}
+		}
+	}while(again);
+
+	/*
+	 * Clear bits for useless transitions.  We could do this during
+	 * the switch above, but then we have no guarantee of termination
+	 * if we get a loop in the regexp.
+	 */
+	for(i=d->p->firstinst, k=0; k < d->ninst; i++, k++){
+		if(!bits[k])
+			continue;
+		switch(i->type){
+		case RBRA:
+		case LBRA:
+		case OR:
+		case BOL:
+		case EOL:
+			bits[k] = 0;
+			break;
+		}
+	}
+}
+
+/*
+ * Where does s go if it sees rune r?
+ * Eol is true if a $ matches the string at the position just after r.
+ */
+static Reiset*
+transition(Deter *d, Reiset *s, Rune r, uint eol)
+{
+	int k;
+	uchar *bits;
+	Reinst *i, *inst0;
+	Rune *rp, *ep;
+
+	bits = d->bits;
+	memset(bits, 0, d->ninst);
+
+	inst0 = d->p->firstinst;
+	for(k=0; k < s->ninst; k++){
+		i = inst0 + s->inst[k];
+		switch(i->type){
+		default:
+			werrstr("bad reprog: got type %d", i->type);
+			longjmp(d->kaboom, 1);
+		case RBRA:
+		case LBRA:
+		case OR:
+		case BOL:
+		case EOL:
+			werrstr("internal error: got type %d", i->type);
+			longjmp(d->kaboom, 1);
+
+		case RUNE:
+			if(r == i->r)
+				bits[i->next - inst0] = 1;
+			break;
+		case ANY:
+			if(r != L'\n')
+				bits[i->next - inst0] = 1;
+			break;
+		case ANYNL:
+			bits[i->next - inst0] = 1;
+			break;
+		case NCCLASS:
+			if(r == L'\n')
+				break;
+			/* fall through */
+		case CCLASS:
+			ep = i->cp->end;
+			for(rp = i->cp->spans; rp < ep; rp += 2)
+				if(rp[0] <= r && r <= rp[1])
+					break;
+			if((rp < ep) ^! (i->type == CCLASS))
+				bits[i->next - inst0] = 1;
+			break;
+		case END:
+			break;
+		}
+	}
+
+	followempty(d, bits, r=='\n', eol);
+	return bits2reiset(d, bits);
+}
+
+static int
+countinst(Reprog *pp)
+{
+	int n;
+	Reinst *l;
+
+	n = 0;
+	l = pp->firstinst;
+	while(l++->type)
+		n++;
+	return n;
+}
+
+static void
+set(Deter *d, u32int **tab, Rune r)
+{
+	u32int *u;
+
+	if((u = tab[r/4096]) == nil){
+		u = binalloc(&d->bin, 4096/8, 1);
+		if(u == nil)
+			longjmp(d->kaboom, 1);
+		tab[r/4096] = u;
+	}
+	u[(r%4096)/32] |= 1<<(r%32);
+}
+
+/*
+ * Compute the list of important characters. 
+ * Other characters behave like the ones that surround them.
+ */
+static void
+findchars(Deter *d, Reprog *p)
+{
+	u32int *tab[65536/4096], *u, x;
+	Reinst *i;
+	Rune *rp, *ep;
+	int k, m, n, a;
+
+	memset(tab, 0, sizeof tab);
+	set(d, tab, 0);
+	set(d, tab, 0xFFFF);
+	for(i=p->firstinst; i->type; i++){
+		switch(i->type){
+		case ANY:
+			set(d, tab, L'\n'-1);
+			set(d, tab, L'\n');
+			set(d, tab, L'\n'+1);
+			break;
+		case RUNE:
+			set(d, tab, i->r-1);
+			set(d, tab, i->r);
+			set(d, tab, i->r+1);
+			break;
+		case NCCLASS:
+			set(d, tab, L'\n'-1);
+			set(d, tab, L'\n');
+			set(d, tab, L'\n'+1);
+			/* fall through */
+		case CCLASS:
+			ep = i->cp->end;
+			for(rp = i->cp->spans; rp < ep; rp += 2){
+				set(d, tab, rp[0]-1);
+				set(d, tab, rp[0]);
+				set(d, tab, rp[1]);
+				set(d, tab, rp[1]+1);
+			}
+			break;
+		}
+	}
+
+	n = 0;
+	for(k=0; k<nelem(tab); k++){
+		if((u = tab[k]) == nil)
+			continue;
+		for(m=0; m<4096/32; m++){
+			if((x = u[m]) == 0)
+				continue;
+			for(a=0; a<32; a++)
+				if(x&(1<<a))
+					n++;
+		}
+	}
+
+	d->c = binalloc(&d->bin, (n+1)*sizeof(Rune), 0);
+	if(d->c == 0)
+		longjmp(d->kaboom, 1);
+	d->nc = n;
+
+	n = 0;
+	for(k=0; k<nelem(tab); k++){
+		if((u = tab[k]) == nil)
+			continue;
+		for(m=0; m<4096/32; m++){
+			if((x = u[m]) == 0)
+				continue;
+			for(a=0; a<32; a++)
+				if(x&(1<<a))
+					d->c[n++] = k*4096+m*32+a;
+		}
+	}
+
+	d->c[n] = 0;
+	if(n != d->nc)
+		abort();
+}
+
+/*
+ * convert the Deter and Reisets into a Dreprog.
+ * if dp and c are nil, just return the count of Drecases needed.
+ */
+static int
+buildprog(Deter *d, Reiset **id2set, int nid, Dreprog *dp, Drecase *c)
+{
+	int i, j, id, n, nn;
+	Dreinst *di;
+	Reiset *s;
+
+	nn = 0;
+	di = 0;
+	for(i=0; i<nid; i++){
+		s = id2set[i];
+		if(c){
+			di = &dp->inst[i];
+			di->isfinal = s->isfinal;
+		}
+		n = 0;
+		id = -1;
+		for(j=0; j<2*d->nc; j++){
+			if(s->delta[j]->id != id){
+				id = s->delta[j]->id;
+				if(c){
+					c[n].start = ((j/d->nc)<<16) | d->c[j%d->nc];
+					c[n].next = &dp->inst[id];
+				}
+				n++;
+			}
+		}
+		if(c){
+			if(n == 1 && c[0].next == di)
+				di->isloop = 1;
+			di->c = c;
+			di->nc = n;
+			c += n;
+		}
+		nn += n;
+	}
+	return nn;
+}
+
+Dreprog*
+dregcvt(Reprog *p)
+{
+	uchar *bits;
+	uint again, n, nid, id;
+	Deter d;
+	Reiset **id2set, *s, *t, *start[4];
+	Dreprog *dp;
+	Drecase *c;
+
+	memset(&d, 0, sizeof d);
+
+	if(setjmp(d.kaboom)){
+		binfree(&d.bin);
+		return nil;
+	}
+
+	d.p = p;
+	d.ninst = countinst(p);
+
+	d.last = &d.alloc;
+
+	n = d.ninst;
+	/* round up to power of two; this loop is the least of our efficiency problems */
+	while(n&(n-1))
+		n++;
+	d.nhash = n;
+	d.hash = binalloc(&d.bin, d.nhash*sizeof(Reinst*), 1);
+
+	/* get list of important runes */
+	findchars(&d, p);
+
+#ifdef DUMP
+	print("relevant chars are: «%S»\n", d.c+1);
+#endif
+
+	d.bits = bits = binalloc(&d.bin, d.ninst, 0);
+	d.tmp = ralloc(&d, d.ninst);
+
+	/*
+	 * Convert to DFA
+	 */
+
+	/* 4 start states, depending on initial bol, eol */
+	for(n=0; n<4; n++){
+		memset(bits, 0, d.ninst);
+		bits[p->startinst - p->firstinst] = 1;
+		followempty(&d, bits, n&1, n&2);
+		start[n] = bits2reiset(&d, bits);
+	}
+
+	/* explore the reiset space */
+	for(s=d.alloc; s; s=s->next)
+		for(n=0; n<2*d.nc; n++)
+			s->delta[n] = transition(&d, s, d.c[n%d.nc], n/d.nc);
+
+#ifdef DUMP
+	nid = 0;
+	for(s=d.alloc; s; s=s->next)
+		s->id = nid++;
+	ddump(&d);
+#endif
+
+	/*
+	 * Minimize.
+	 */
+
+	/* first class division is final or not */
+	for(s=d.alloc; s; s=s->next){
+		s->isfinal = 0;
+		for(n=0; n<s->ninst; n++)
+			if(p->firstinst[s->inst[n]].type == END)
+				s->isfinal = 1;
+		s->id = s->isfinal;
+	}
+
+	/* divide states with different transition tables in id space */
+	nid = 2;
+	do{
+		again = 0;
+		for(s=d.alloc; s; s=s->next){
+			id = -1;
+			for(t=s->next; t; t=t->next){
+				if(s->id != t->id)
+					continue;
+				for(n=0; n<2*d.nc; n++){
+					/* until we finish the for(t) loop, s->id and id are same */
+					if((s->delta[n]->id == t->delta[n]->id)
+					|| (s->delta[n]->id == s->id && t->delta[n]->id == id)
+					|| (s->delta[n]->id == id && t->delta[n]->id == s->id))
+						continue;
+					break;
+				}
+				if(n == 2*d.nc)
+					continue;
+				if(id == -1)
+					id = nid++;
+				t->id = id;
+				again = 1;
+			}
+		}
+	}while(again);
+
+#ifdef DUMP
+	ddump(&d);
+#endif
+
+	/* build dreprog */
+	id2set = binalloc(&d.bin, nid*sizeof(Reiset*), 1);
+	if(id2set == nil)
+		longjmp(d.kaboom, 1);
+	for(s=d.alloc; s; s=s->next)
+		id2set[s->id] = s;
+
+	n = buildprog(&d, id2set, nid, nil, nil);
+	dp = mallocz(sizeof(Dreprog)+nid*sizeof(Dreinst)+n*sizeof(Drecase), 1);
+	if(dp == nil)
+		longjmp(d.kaboom, 1);
+	c = (Drecase*)&dp->inst[nid];
+	buildprog(&d, id2set, nid, dp, c);
+
+	for(n=0; n<4; n++)
+		dp->start[n] = &dp->inst[start[n]->id];
+	dp->ninst = nid;
+
+	binfree(&d.bin);
+	return dp;
+}
+
+int
+dregexec(Dreprog *p, char *s, int bol)
+{
+	Rune r;
+	ulong rr;
+	Dreinst *i;
+	Drecase *c, *ec;
+	int best, n;
+	char *os;
+
+	i = p->start[(bol ? 1 : 0) | (s[1]=='\n' ? 2 : 0)];
+	best = -1;
+	os = s;
+	for(; *s; s+=n){
+		if(i->isfinal)
+			best = s - os;
+		if(i->isloop){
+			if(i->isfinal)
+				return strlen(os);
+			else
+				return best;
+		}
+		if((*s&0xFF) < Runeself){
+			r = *s;
+			n = 1;
+		}else
+			n = chartorune(&r, s);
+		c = i->c;
+		ec = c+i->nc;
+		rr = r;
+		if(s[n] == '\n' || s[n] == '\0')
+			rr |= 0x10000;
+		for(; c<ec; c++){
+			if(c->start > rr){
+				i = c[-1].next;
+				goto Out;
+			}
+		}
+		i = ec[-1].next;
+	Out:;
+	}
+	if(i->isfinal)
+		best = s - os;
+	return best;
+}
+
+
+#ifdef DUMP
+void
+ddump(Deter *d)
+{
+	int i, id;
+	Reiset *s;
+
+	for(s=d->alloc; s; s=s->next){
+		print("%d ", s->id);
+		id = -1;
+		for(i=0; i<2*d->nc; i++){
+			if(id != s->delta[i]->id){
+				if(i==0)
+					print(" [");
+				else if(i/d->nc)
+					print(" [%C$", d->c[i%d->nc]);
+				else
+					print(" [%C", d->c[i%d->nc]);
+				print(" %d]", s->delta[i]->id);
+				id = s->delta[i]->id;
+			}
+		}
+		print("\n");
+	}
+}
+
+void
+rdump(Reprog *pp)
+{
+	Reinst *l;
+	Rune *p;
+
+	l = pp->firstinst;
+	do{
+		print("%ld:\t0%o\t%ld\t%ld", l-pp->firstinst, l->type,
+			l->left-pp->firstinst, l->right-pp->firstinst);
+		if(l->type == RUNE)
+			print("\t%C\n", l->r);
+		else if(l->type == CCLASS || l->type == NCCLASS){
+			print("\t[");
+			if(l->type == NCCLASS)
+				print("^");
+			for(p = l->cp->spans; p < l->cp->end; p += 2)
+				if(p[0] == p[1])
+					print("%C", p[0]);
+				else
+					print("%C-%C", p[0], p[1]);
+			print("]\n");
+		} else
+			print("\n");
+	}while(l++->type);
+}
+
+void
+dump(Dreprog *pp)
+{
+	int i, j;
+	Dreinst *l;
+
+	print("start %ld %ld %ld %ld\n",
+		pp->start[0]-pp->inst,
+		pp->start[1]-pp->inst,
+		pp->start[2]-pp->inst,
+		pp->start[3]-pp->inst);
+
+	for(i=0; i<pp->ninst; i++){
+		l = &pp->inst[i];
+		print("%d:", i);
+		for(j=0; j<l->nc; j++){
+			print(" [");
+			if(j == 0)
+				if(l->c[j].start != 1)
+					abort();
+			if(j != 0)
+				print("%C%s", l->c[j].start&0xFFFF, (l->c[j].start&0x10000) ? "$" : "");
+			print("-");
+			if(j != l->nc-1)
+				print("%C%s", (l->c[j+1].start&0xFFFF)-1, (l->c[j+1].start&0x10000) ? "$" : "");
+			print("] %ld", l->c[j].next - pp->inst);
+		}
+		if(l->isfinal)
+			print(" final");
+		if(l->isloop)
+			print(" loop");
+		print("\n");
+	}
+}
+
+
+void
+main(int argc, char **argv)
+{
+	int i;
+	Reprog *p;
+	Dreprog *dp;
+
+	i = 1;
+		p = regcomp(argv[i]);
+		if(p == 0){
+			print("=== %s: bad regexp\n", argv[i]);
+		}
+	//	print("=== %s\n", argv[i]);
+	//	rdump(p);
+		dp = dregcvt(p);
+		print("=== dfa\n");
+		dump(dp);
+	
+	for(i=2; i<argc; i++)
+		print("match %d\n", dregexec(dp, argv[i], 0));
+	exits(0);
+}
+#endif
+
+void
+Bprintdfa(Biobuf *b, Dreprog *p)
+{
+	int i, j, nc;
+
+	Bprint(b, "# dreprog\n");
+	nc = 0;
+	for(i=0; i<p->ninst; i++)
+		nc += p->inst[i].nc;
+	Bprint(b, "%d %d %zd %zd %zd %zd\n", p->ninst, nc,
+		p->start[0]-p->inst, p->start[1]-p->inst,
+		p->start[2]-p->inst, p->start[3]-p->inst);
+	for(i=0; i<p->ninst; i++){
+		Bprint(b, "%d %d %d", p->inst[i].isfinal, p->inst[i].isloop, p->inst[i].nc);
+		for(j=0; j<p->inst[i].nc; j++)
+			Bprint(b, " %d %zd", p->inst[i].c[j].start, p->inst[i].c[j].next-p->inst);
+		Bprint(b, "\n");
+	}
+}
+
+static char*
+egetline(Biobuf *b, int c, jmp_buf jb)
+{
+	char *p;
+
+	p = Brdline(b, c);
+	if(p == nil)
+		longjmp(jb, 1);
+	p[Blinelen(b)-1] = '\0';
+	return p;
+}
+
+static void
+egetc(Biobuf *b, int c, jmp_buf jb)
+{
+	if(Bgetc(b) != c)
+		longjmp(jb, 1);
+}
+
+static int
+egetnum(Biobuf *b, int want, jmp_buf jb)
+{
+	int c;
+	int n, first;
+
+	n = 0;
+	first = 1;
+	while((c = Bgetc(b)) != Beof){
+		if(c < '0' || c > '9'){
+			if(want == 0){
+				Bungetc(b);
+				c = 0;
+			}
+			if(first || c != want){
+				werrstr("format error");
+				longjmp(jb, 1);
+			}
+			return n;
+		}
+		n = n*10 + c - '0';
+		first = 0;
+	}
+	werrstr("unexpected eof");
+	longjmp(jb, 1);
+	return -1;
+}
+
+Dreprog*
+Breaddfa(Biobuf *b)
+{
+	char *s;
+	int ninst, nc;
+	jmp_buf jb;
+	Dreprog *p;
+	Drecase *c;
+	Dreinst *l;
+	int j, k;
+
+	p = nil;
+	if(setjmp(jb)){
+		free(p);
+		return nil;
+	}
+
+	s = egetline(b, '\n', jb);
+	if(strcmp(s, "# dreprog") != 0){
+		werrstr("format error");
+		longjmp(jb, 1);
+	}
+
+	ninst = egetnum(b, ' ', jb);
+	nc = egetnum(b, ' ', jb);
+
+	p = mallocz(sizeof(Dreprog)+ninst*sizeof(Dreinst)+nc*sizeof(Drecase), 1);
+	if(p == nil)
+		longjmp(jb, 1);
+	c = (Drecase*)&p->inst[ninst];
+
+	p->start[0] = &p->inst[egetnum(b, ' ', jb)];
+	p->start[1] = &p->inst[egetnum(b, ' ', jb)];
+	p->start[2] = &p->inst[egetnum(b, ' ', jb)];
+	p->start[3] = &p->inst[egetnum(b, '\n', jb)];
+
+	for(j=0; j<ninst; j++){
+		l = &p->inst[j];
+		l->isfinal = egetnum(b, ' ', jb);
+		l->isloop = egetnum(b, ' ', jb);
+		l->nc = egetnum(b, 0, jb);
+		l->c = c;
+		for(k=0; k<l->nc; k++){
+			egetc(b, ' ', jb);
+			c->start = egetnum(b, ' ', jb);
+			c->next = &p->inst[egetnum(b, 0, jb)];
+			c++;
+		}
+		egetc(b, '\n', jb);
+	}
+	return p;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/dfa.h
@@ -1,0 +1,33 @@
+/*
+ * Deterministic regexp program.
+ */
+typedef struct Dreprog Dreprog;
+typedef struct Dreinst Dreinst;
+typedef struct Drecase Drecase;
+
+struct Dreinst
+{
+	int isfinal;
+	int isloop;
+	Drecase *c;
+	int nc;
+};
+
+struct Dreprog
+{
+	Dreinst *start[4];
+	int ninst;
+	Dreinst inst[1];
+};
+
+struct Drecase
+{
+	uint start;
+	Dreinst *next;
+};
+
+Dreprog* dregcvt(Reprog*);
+int dregexec(Dreprog*, char*, int);
+Dreprog* Breaddfa(Biobuf *b);
+void Bprintdfa(Biobuf*, Dreprog*);
+
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/dump.c
@@ -1,0 +1,67 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "regexp.h"
+#include "regcomp.h"
+#include "dfa.h"
+
+#define DUMP
+
+void
+dump(Dreprog *pp)
+{
+	int i, j;
+	Dreinst *l;
+
+	print("start %ld %ld %ld %ld\n",
+		pp->start[0]-pp->inst,
+		pp->start[1]-pp->inst,
+		pp->start[2]-pp->inst,
+		pp->start[3]-pp->inst);
+
+	for(i=0; i<pp->ninst; i++){
+		l = &pp->inst[i];
+		print("%d:", i);
+		for(j=0; j<l->nc; j++){
+			print(" [");
+			if(j == 0)
+				if(l->c[j].start > 1)
+					print("<bad start %d>\n", l->c[j].start);
+			if(j != 0)
+				print("%C%s", l->c[j].start&0xFFFF, (l->c[j].start&0x10000) ? "$" : "");
+			print("-");
+			if(j != l->nc-1)
+				print("%C%s", (l->c[j+1].start&0xFFFF)-1, (l->c[j+1].start&0x10000) ? "$" : "");
+			print("] %ld", l->c[j].next - pp->inst);
+		}
+		if(l->isfinal)
+			print(" final");
+		if(l->isloop)
+			print(" loop");
+		print("\n");
+	}
+}
+
+
+void
+main(int argc, char **argv)
+{
+	int i;
+	Reprog *p;
+	Dreprog *dp;
+
+	i = 1;
+		p = regcomp(argv[i]);
+		if(p == 0){
+			print("=== %s: bad regexp\n", argv[i]);
+		}
+	//	print("=== %s\n", argv[i]);
+	//	rdump(p);
+		dp = dregcvt(p);
+		print("=== dfa\n");
+		dump(dp);
+	
+	for(i=2; i<argc; i++)
+		print("match %d\n", dregexec(dp, argv[i], 1));
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/hash.c
@@ -1,0 +1,312 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "hash.h"
+
+/***
+ * String hash tables.
+ */
+
+Stringtab *tfree;
+
+Stringtab*
+taballoc(void)
+{
+	static Stringtab *t;
+	static uint nt;
+
+	if(tfree){
+		Stringtab *tt = tfree;
+		tfree = tt->link;
+		return tt;
+	}
+
+	if(nt == 0){
+		t = malloc(64000*sizeof(Stringtab));
+		if(t == 0)
+			sysfatal("out of memory");
+		nt = 64000;
+	}
+	nt--;
+	return t++;
+}
+
+void
+tabfree(Stringtab *tt)
+{
+	tt->link = tfree;
+	tfree = tt;
+}
+
+char*
+xstrdup(char *s, int len)
+{
+	char *r;
+	static char *t;
+	static int nt;
+
+	if(nt < len){
+		t = malloc(512*1024+len);
+		if(t == 0)
+			sysfatal("out of memory");
+		nt = 512*1024;
+	}
+	r = t;
+	t += len;
+	nt -= len;
+	memmove(r, s, len);
+	return r;
+}
+
+static uint
+hash(char *s, int n)
+{
+	uint h;
+	uchar *p, *ep;
+	h = 0;
+	for(p=(uchar*)s, ep=p+n; p<ep; p++)
+		h = h*37 + *p;
+	return h;
+}
+
+static void
+rehash(Hash *hh)
+{
+	int h;
+	Stringtab *s;
+
+	if(hh->nstab == 0)
+		hh->nstab = 1024;
+	else
+		hh->nstab = hh->ntab*2;
+
+	free(hh->stab);
+	hh->stab = mallocz(hh->nstab*sizeof(Stringtab*), 1);
+	if(hh->stab == nil)
+		sysfatal("out of memory");
+
+	for(s=hh->all; s; s=s->link){
+		h = hash(s->str, s->n) % hh->nstab;
+		s->hash = hh->stab[h];
+		hh->stab[h] = s;
+	}
+}
+
+Stringtab*
+findstab(Hash *hh, char *str, int n, int create)
+{
+	uint h;
+	Stringtab *tab, **l;
+
+	if(hh->nstab == 0)
+		rehash(hh);
+
+	h = hash(str, n) % hh->nstab;
+	for(tab=hh->stab[h], l=&hh->stab[h]; tab; l=&tab->hash, tab=tab->hash)
+		if(n==tab->n && memcmp(str, tab->str, n) == 0){
+			*l = tab->hash;
+			tab->hash = hh->stab[h];
+			hh->stab[h] = tab;
+			return tab;
+		}
+
+	if(!create)
+		return nil;
+
+	hh->sorted = 0;
+	tab = taballoc();
+	tab->str = xstrdup(str, n);
+	tab->hash = hh->stab[h];
+	tab->link = hh->all;
+	hh->all = tab;
+	tab->n = n;
+	tab->count = 0;
+	tab->date = 0;
+	hh->stab[h] = tab;
+
+	hh->ntab++;
+	if(hh->ntab > 2*hh->nstab && !(hh->ntab&(hh->ntab-1)))
+		rehash(hh);
+	return tab;
+}
+
+int
+scmp(Stringtab *a, Stringtab *b)
+{
+	int n, x;
+
+	if(a == 0)
+		return 1;
+	if(b == 0)
+		return -1;
+	n = a->n;
+	if(n > b->n)
+		n = b->n;
+	x = memcmp(a->str, b->str, n);
+	if(x != 0)
+		return x;
+	if(a->n < b->n)
+		return -1;
+	if(a->n > b->n)
+		return 1;
+	return 0;	/* shouldn't happen */
+}
+
+Stringtab*
+merge(Stringtab *a, Stringtab *b)
+{
+	Stringtab *s, **l;
+
+	l = &s;
+	while(a || b){
+		if(scmp(a, b) < 0){
+			*l = a;
+			l = &a->link;
+			a = a->link;
+		}else{
+			*l = b;
+			l = &b->link;
+			b = b->link;
+		}
+	}
+	*l = 0;
+	return s;
+}
+
+Stringtab*
+mergesort(Stringtab *s)
+{
+	Stringtab *a, *b;
+	int delay;
+
+	if(s == nil)
+		return nil;
+	if(s->link == nil)
+		return s;
+
+	a = b = s;
+	delay = 1;
+	while(a && b){
+		if(delay)	/* easy way to handle 2-element list */
+			delay = 0;
+		else
+			a = a->link;
+		if(b = b->link)
+			b = b->link;
+	}
+
+	b = a->link;
+	a->link = nil;
+
+	a = mergesort(s);
+	b = mergesort(b);
+
+	return merge(a, b);
+}
+
+Stringtab*
+sortstab(Hash *hh)
+{
+	if(!hh->sorted){
+		hh->all = mergesort(hh->all);
+		hh->sorted = 1;
+	}
+	return hh->all;
+}
+
+int
+Bwritehash(Biobuf *b, Hash *hh)
+{
+	Stringtab *s;
+	int now;
+
+	now = time(0);
+	s = sortstab(hh);
+	Bprint(b, "# hash table\n");
+	for(; s; s=s->link){
+		if(s->count <= 0)
+			continue;
+		/*
+		 * Entries that haven't been updated in thirty days get tossed.
+		 */
+		if(s->date+30*86400 < now)
+			continue;
+		Bwrite(b, s->str, s->n);
+		Bprint(b, "\t%d %d\n", s->count, s->date);
+	}
+	if(Bflush(b) == Beof)
+		return -1;
+	return 0;
+}
+
+void
+Breadhash(Biobuf *b, Hash *hh, int scale)
+{
+	char *s;
+	char *t;
+	int n;
+	int date;
+	Stringtab *st;
+
+	s = Brdstr(b, '\n', 1);
+	if(s == nil)
+		return;
+	if(strcmp(s, "# hash table") != 0)
+		sysfatal("bad hash table format");
+
+	while(s = Brdline(b, '\n')){
+		s[Blinelen(b)-1] = 0;
+		t = strrchr(s, '\t');
+		if(t == nil)
+			sysfatal("bad hash table format");
+		*t++ = '\0';
+		if(*t < '0' || *t > '9')
+			sysfatal("bad hash table format");
+		n = strtol(t, &t, 10);
+		date = time(0);
+		if(*t != 0){
+			if(*t == ' '){
+				t++;
+				date = strtol(t, &t, 10);
+			}
+			if(*t != 0)
+				sysfatal("bad hash table format");
+		}
+		st = findstab(hh, s, strlen(s), 1);
+		if(date > st->date)
+			st->date = date;
+		st->count += n*scale;
+	}
+}
+
+void
+freehash(Hash *h)
+{
+	Stringtab *s, *next;
+
+	for(s=h->all; s; s=next){
+		next = s->link;
+		tabfree(s);
+	}
+	free(h->stab);
+	free(h);
+}
+
+Biobuf*
+Bopenlock(char *file, int mode)
+{
+	int i;
+	Biobuf *b;
+	char err[ERRMAX];
+
+	b = nil;
+	for(i=0; i<120; i++){
+		if((b = Bopen(file, mode)) != nil)
+			break;
+		rerrstr(err, sizeof err);
+		if(strstr(err, "file is locked")==nil && strstr(err, "exclusive lock")==nil)
+			break;
+		sleep(1000);
+	}
+	return b;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/hash.h
@@ -1,0 +1,27 @@
+typedef struct Stringtab	Stringtab;
+struct Stringtab {
+	Stringtab *link;
+	Stringtab *hash;
+	char *str;
+	int n;
+	int count;
+	int date;
+};
+
+typedef struct Hash Hash;
+struct Hash
+{
+	int sorted;
+	Stringtab **stab;
+	int nstab;
+	int ntab;
+	Stringtab *all;
+};
+
+Stringtab *findstab(Hash*, char*, int, int);
+Stringtab *sortstab(Hash*);
+
+int Bwritehash(Biobuf*, Hash*);	/* destroys hash */
+void Breadhash(Biobuf*, Hash*, int);
+void freehash(Hash*);
+Biobuf *Bopenlock(char*, int);
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/mkfile
@@ -1,0 +1,28 @@
+</$objtype/mkfile
+
+TARG=addhash bayes msgtok
+
+</sys/src/cmd/mkmany
+<../mkupas
+
+# msg tokenizer
+$O.regen: regcomp.$O dfa.$O
+dfa.$O regcomp.$O regen.$O: dfa.h
+
+/mail/lib/classify.re: $O.regen
+	if(~ $cputype $objtype)
+		$O.regen >x && cp x $target
+
+$O.msgtok: dfa.$O
+
+# msg database 
+msgdbx.$O msgdb.$O: msgdb.h
+
+# hash table creator/dumper
+$O.msgdb: msgdbx.$O
+
+$O.msgclass: hash.$O msgdbx.$O
+
+$O.addhash: hash.$O 
+
+$O.bayes: hash.$O
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/msgclass.c
@@ -1,0 +1,296 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ctype.h>
+#include "msgdb.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: upas/msgclass [-a] [-d name dbfile]... [-l lockfile] [-m mul] [-t thresh] [tokenfile ...]\n");
+	exits("usage");
+}
+
+enum
+{
+	MAXBEST = 32,
+	MAXLEN = 64,
+	MAXTAB = 256,
+};
+
+typedef struct Ndb Ndb;
+struct Ndb
+{
+	char *name;
+	char *file;
+	Msgdb *db;
+	double p;
+	long nmsg;
+};
+
+typedef struct Word Word;
+struct Word
+{
+	char s[MAXLEN];
+	int count[MAXTAB];
+	double p[MAXTAB];
+	double mp;
+	int mi; /* w.p[w.mi] = w.mp */
+	int nmsg;
+};
+
+Ndb db[MAXTAB];
+int ndb;
+
+int add;
+int mul;
+Msgdb *indb;
+
+Word best[MAXBEST];
+int mbest = 15;
+int nbest;
+
+void process(Biobuf*, char*);
+void lockfile(char*);
+
+void
+noteword(Word *w, char *s)
+{
+	int i;
+
+	for(i=nbest-1; i>=0; i--)
+		if(w->mp < best[i].mp)
+			break;
+	i++;
+
+	if(i >= mbest)
+		return;
+	if(nbest == mbest)
+		nbest--;
+	if(i < nbest)
+		memmove(&best[i+1], &best[i], (nbest-i)*sizeof(best[0]));
+	best[i] = *w;
+	strecpy(best[i].s, best[i].s+MAXLEN, s);
+	nbest++;
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, bad, m, tot, nn, j;
+	Biobuf bin, *b, bout;
+	char *s, *lf;
+	double totp, p, thresh;
+	long n;
+	Word w;
+
+	lf = nil;
+	thresh = 0;
+	ARGBEGIN{
+	case 'a':
+		add = 1;
+		break;
+	case 'd':
+		if(ndb >= MAXTAB)
+			sysfatal("too many db classes");
+		db[ndb].name = EARGF(usage());
+		db[ndb].file = EARGF(usage());
+		ndb++;
+		break;
+	case 'l':
+		lf = EARGF(usage());
+		break;
+	case 'm':
+		mul = atoi(EARGF(usage()));
+		break;
+	case 't':
+		thresh = atof(EARGF(usage()));
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(ndb == 0){
+		fprint(2, "must have at least one -d option\n");
+		usage();
+	}
+
+	indb = mdopen(nil, 1);
+	if(argc == 0){
+		Binit(&bin, 0, OREAD);
+		process(&bin, "<stdin>");
+		Bterm(&bin);
+	}else{
+		bad = 0;
+		for(i=0; i<argc; i++){
+			if((b = Bopen(argv[i], OREAD)) == nil){
+				fprint(2, "opening %s: %r\n", argv[i]);
+				bad = 1;
+				continue;
+			}
+			process(b, argv[i]);
+			Bterm(b);
+		}
+		if(bad)
+			exits("open inputs");
+	}
+
+	lockfile(lf);
+	bad = 0;
+	for(i=0; i<ndb; i++){
+		if((db[i].db = mdopen(db[i].file, 0)) == nil){
+			fprint(2, "opendb %s: %r\n", db[i].file);
+			bad = 1;
+		}
+		db[i].nmsg = mdget(db[i].db, "*From*");
+	}
+	if(bad)
+		exits("open databases");
+
+	/* run conditional probabilities of input words, getting 15 most specific */
+	mdenum(indb);
+	nbest = 0;
+	while(mdnext(indb, &s, &n) >= 0){
+		tot = 0;
+		totp = 0.0;
+		for(i=0; i<ndb; i++){
+			nn = mdget(db[i].db, s)*(i==0 ? 3 : 1);
+			tot += nn;
+			w.count[i] = nn;
+			p = w.count[i]/(double)db[i].nmsg;
+			if(p >= 1.0)
+				p = 1.0;
+			w.p[i] = p;
+			totp += p;
+		}
+//fprint(2, "%s tot %d totp %g\n", s, tot, totp);
+		if(tot < 2)
+			continue;
+		w.mp = 0.0;
+		for(i=0; i<ndb; i++){
+			p = w.p[i];
+			p /= totp;
+			if(p < 0.001)
+				p = 0.001;
+			else if(p > 0.999)
+				p = 0.999;
+			if(p > w.mp){
+				w.mp = p;
+				w.mi = i;
+			}
+			w.p[i] = p;
+		}
+		noteword(&w, s);
+	}
+
+	/* compute conditional probabilities of message classes using 15 most specific */
+	totp = 0.0;
+	for(i=0; i<ndb; i++){
+		p = 1.0;
+		for(j=0; j<nbest; j++)
+			p *= best[j].p[i];
+		db[i].p = p;
+		totp += p;
+	}
+	for(i=0; i<ndb; i++)
+		db[i].p /= totp;
+	m = 0;
+	for(i=1; i<ndb; i++)
+		if(db[i].p > db[m].p)
+			m = i;
+
+	Binit(&bout, 1, OWRITE);
+	if(db[m].p < thresh)
+		m = -1;
+	if(m >= 0)
+		Bprint(&bout, "%s", db[m].name);
+	else
+		Bprint(&bout, "inconclusive");
+	for(j=0; j<ndb; j++)
+		Bprint(&bout, " %s=%g", db[j].name, db[j].p);
+	Bprint(&bout, "\n");
+	for(i=0; i<nbest; i++){
+		Bprint(&bout, "%s", best[i].s);
+		for(j=0; j<ndb; j++)
+			Bprint(&bout, " %s=%g", db[j].name, best[i].p[j]);
+		Bprint(&bout, "\n");
+	}
+		Bprint(&bout, "%s %g\n", best[i].s, best[i].p[m]);
+	Bterm(&bout);
+
+	if(m >= 0 && add){
+		mdenum(indb);
+		while(mdnext(indb, &s, &n) >= 0)
+			mdput(db[m].db, s, mdget(db[m].db, s)+n*mul);
+		mdclose(db[m].db);
+	}
+	exits(nil);
+}
+
+void
+process(Biobuf *b, char*)
+{
+	char *s;
+	char *p;
+	long n;
+
+	while((s = Brdline(b, '\n')) != nil){
+		s[Blinelen(b)-1] = 0;
+		if((p = strrchr(s, ' ')) != nil){
+			*p++ = 0;
+			n = atoi(p);
+		}else
+			n = 1;
+		mdput(indb, s, mdget(indb, s)+n);
+	}
+}
+
+int tpid;
+void
+killtickle(void)
+{
+	postnote(PNPROC, tpid, "die");
+}
+
+void
+lockfile(char *s)
+{
+	int fd, t, w;
+	char err[ERRMAX];
+
+	if(s == nil)
+		return;
+	w = 50;
+	t = 0;
+	for(;;){
+		fd = open(s, OREAD);
+		if(fd >= 0)
+			break;
+		rerrstr(err, sizeof err);
+		if(strstr(err, "file is locked")==nil && strstr(err, "exclusive lock")==nil))
+			break;
+		sleep(w);
+		t += w;
+		if(w < 1000)
+			w = (w*3)/2;
+		if(t > 120*1000)
+			break;
+	}
+	if(fd < 0)
+		sysfatal("could not lock %s", s);
+	switch(tpid = fork()){
+	case -1:
+		sysfatal("fork: %r");
+	case 0:
+		for(;;){
+			sleep(30*1000);
+			free(dirfstat(fd));
+		}
+		_exits(nil);
+	default:
+		break;
+	}
+	close(fd);
+	atexit(killtickle);
+}
+
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/msgdb.c
@@ -1,0 +1,63 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "msgdb.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: msgdb [-c] file\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int create = 0;
+	Msgdb *db;
+	char *tok, *p;
+	long val;
+	int input;
+	Biobuf b;
+
+	input = 0;
+	ARGBEGIN{
+	case 'c':
+		create = 1;
+		break;
+	case 'i':
+		input = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+
+	if((db = mdopen(argv[0], create)) == nil)
+		sysfatal("open db: %r");
+
+	if(input){
+		Binit(&b, 0, OREAD);
+		while((tok = Brdline(&b, '\n')) != nil){
+			tok[Blinelen(&b)-1] = '\0';
+			p = strrchr(tok, ' ');
+			if(p == nil)
+				val = mdget(db, tok)+1;
+			else{
+				*p++ = 0;
+				val = atoi(p);
+			}
+			mdput(db, tok, val);
+		}
+	}else{
+		mdenum(db);
+		Binit(&b, 1, OWRITE);
+		while(mdnext(db, &tok, &val) >= 0)
+			Bprint(&b, "%s %ld\n", tok, val);
+		Bterm(&b);
+	}
+	mdclose(db);
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/msgdb.h
@@ -1,0 +1,10 @@
+typedef struct Msgdb Msgdb;
+
+Msgdb *mdopen(char*, int);
+long mdget(Msgdb*, char*);
+void mdput(Msgdb*, char*, long);
+
+void mdenum(Msgdb*);
+int mdnext(Msgdb*, char**, long*);
+
+void mdclose(Msgdb*);
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/msgdbx.c
@@ -1,0 +1,109 @@
+#include <u.h>
+#include <libc.h>
+#include <db.h>
+#include "msgdb.h"
+
+struct Msgdb
+{
+	DB *db;
+	int reset;
+};
+
+Msgdb*
+mdopen(char *file, int create)
+{
+	Msgdb *mdb;
+	DB *db;
+	HASHINFO h;
+
+	if((mdb = mallocz(sizeof(Msgdb), 1)) == nil)
+		return nil;
+	memset(&h, 0, sizeof h);
+	h.cachesize = 2*1024*1024;
+	if((db = dbopen(file, ORDWR|(create ? OCREATE:0), 0666, DB_HASH, &h)) == nil){
+		free(mdb);
+		return nil;
+	}
+	mdb->db = db;
+	mdb->reset = 1;
+	return mdb;
+}
+
+long
+mdget(Msgdb *mdb, char *tok)
+{
+	DB *db = mdb->db;
+	DBT key, val;
+	uchar *p;
+
+	key.data = tok;
+	key.size = strlen(tok)+1;
+	val.data = 0;
+	val.size = 0;
+
+	if(db->get(db, &key, &val, 0) < 0)
+		return 0;
+	if(val.data == 0)
+		return 0;
+	if(val.size != 4)
+		return 0;
+	p = val.data;
+	return (p[0]<<24)|(p[1]<<16)|(p[2]<<8)|p[3];
+}
+
+void
+mdput(Msgdb *mdb, char *tok, long n)
+{
+	uchar p[4];
+	DB *db = mdb->db;
+	DBT key, val;
+
+	key.data = tok;
+	key.size = strlen(tok)+1;
+	if(n <= 0){
+		db->del(db, &key, 0);
+		return;
+	}
+
+	p[0] = n>>24;
+	p[1] = n>>16;
+	p[2] = n>>8;
+	p[3] = n;
+
+	val.data = p;
+	val.size = 4;
+	db->put(db, &key, &val, 0);
+}
+
+void
+mdenum(Msgdb *mdb)
+{
+	mdb->reset = 1;
+}
+
+int
+mdnext(Msgdb *mdb, char **sp, long *vp)
+{
+	DBT key, val;
+	uchar *p;
+	DB *db = mdb->db;
+	int i;
+
+	i = db->seq(db, &key, &val, mdb->reset ? R_FIRST : R_NEXT);
+	mdb->reset = 0;
+	if(i)
+		return -1;
+	*sp = key.data;
+	p = val.data;
+	*vp = (p[0]<<24)|(p[1]<<16)|(p[2]<<8)|p[3];
+	return 0;
+}
+
+void
+mdclose(Msgdb *mdb)
+{
+	DB *db = mdb->db;
+
+	db->close(db);
+	mdb->db = nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/msgtok.c
@@ -1,0 +1,245 @@
+/*
+ * RFC822 message tokenizer (really feature generator) for spam filter.
+ * 
+ * See Paul Graham's musings on spam filtering for theory.
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "regexp.h"
+#include <ctype.h>
+#include "dfa.h"
+
+void buildre(Dreprog*[3]);
+int debug;
+char *refile = "/mail/lib/classify.re";
+int maxtoklen = 20;
+int trim(char*);
+
+void
+usage(void)
+{
+	fprint(2, "usage: msgtok [-D] [-r /mail/lib/classify.re] [file]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, hdr, n, eof, off;
+	Dreprog *re[3];
+	int m[3];
+	char *p, *ep, *tag;
+	Biobuf bout, bin;
+	char msg[1024+1];
+	char buf[1024];
+
+	buildre(re);
+	ARGBEGIN{
+	case 'D':
+		debug = 1;
+		break;
+	case 'n':
+		maxtoklen = atoi(EARGF(usage()));
+		break;
+	case 'r':
+		refile = EARGF(usage());
+		break;
+	default:
+		usage();
+	}ARGEND;
+
+	if(argc > 1)
+		usage();
+	if(argc == 1){
+		close(0);
+		if(open(argv[0], OREAD) < 0)
+			sysfatal("open %s: %r", argv[0]);
+	}
+
+	tag = nil;
+	Binit(&bin, 0, OREAD);
+	Binit(&bout, 1, OWRITE);
+	ep = msg;
+	p = msg;
+	eof = 0;
+	off = 0;
+	hdr = 1;
+	for(;;){
+		/* replenish buffer */
+		if(ep - p < 512 && !eof){
+			if(p > msg + 1){
+				n = ep - p;
+				memmove(msg, p-1, ep-(p-1));
+				off += (p-1) - msg;
+				p = msg+1;
+				ep = p + n;
+			}
+			n = Bread(&bin, ep, msg+(sizeof msg - 1)- ep);
+			if(n < 0)
+				sysfatal("read error: %r");
+			if(n == 0)
+				eof = 1;
+			ep += n;
+			*ep = 0;
+		}
+		if(p >= ep)
+			break;
+
+		if(*p == 0){
+			p++;
+			continue;
+		}
+
+		if(hdr && p[-1]=='\n'){
+			if(p[0]=='\n')
+				hdr = 0;
+			else if(cistrncmp(p-1, "\nfrom:", 6) == 0)
+				tag = "From*";
+			else if(cistrncmp(p-1, "\nto:", 4) == 0)
+				tag = "To*";
+			else if(cistrncmp(p-1, "\nsubject:", 9) == 0)
+				tag = "Subject*";
+			else if(cistrncmp(p-1, "\nreturn-path:", 13) == 0)
+				tag = "Return-Path*";
+			else
+				tag = nil;
+		}
+		m[0] = dregexec(re[0], p, p==msg || p[-1]=='\n');
+		m[1] = dregexec(re[1], p, p==msg || p[-1]=='\n');
+		m[2] = dregexec(re[2], p, p==msg || p[-1]=='\n');
+
+		n = m[0];
+		if(n < m[1])
+			n = m[1];
+		if(n < m[2])
+			n = m[2];
+		if(n <= 0){
+fprint(2, "«%s» %.2ux", p, p[0]);
+			sysfatal("no regexps matched at %zd", off + (p-msg));
+		}
+
+		if(m[0] >= m[1] && m[0] >= m[2]){
+			/* "From " marks start of new message */
+			Bprint(&bout, "*From*\n");
+			n = m[0];
+			hdr = 1;
+		}else if(m[2] > 1){
+			/* ignore */
+			n = m[2];
+		}else if(m[1] >= m[0] && m[1] >= m[2] && m[1] > 2 && m[1] <= maxtoklen){
+			/* keyword */
+			/* should do UTF-aware lowercasing, too much bother */
+/*
+			for(i=0; i<n; i++)
+				if('A' <= p[i] && p[i] <= 'Z')
+					p[i] += 'a' - 'A';
+*/
+			if(tag){
+				i = strlen(tag);	
+				memmove(buf, tag, i);
+				memmove(buf+i, p, m[1]);
+				buf[i+m[1]] = 0;
+			}else{
+				memmove(buf, p, m[1]);
+				buf[m[1]] = 0;
+			}
+			Bprint(&bout, "%s\n", buf);
+			while(trim(buf) >= 0)
+				Bprint(&bout, "stem*%s\n", buf);
+			n = m[1];
+		}else
+			n = m[2];
+		if(debug)
+			fprint(2, "%.*s¦", utfnlen(p, n), p);
+		p += n;
+	}
+	Bterm(&bout);
+	exits(0);
+}
+
+void
+buildre(Dreprog *re[3])
+{
+	Biobuf *b;
+
+	if((b = Bopen(refile, OREAD)) == nil)
+		sysfatal("open %s: %r", refile);
+
+	re[0] = Breaddfa(b);
+	re[1] = Breaddfa(b);
+	re[2] = Breaddfa(b);
+
+	if(re[0]==nil || re[1]==nil || re[2]==nil)
+		sysfatal("Breaddfa: %r");
+	Bterm(b);
+}
+
+/* perhaps this belongs in the tokenizer */
+int
+trim(char *s)
+{
+	char *p, *op;
+	int mix, mix1;
+
+	if(*s == '*')
+		return -1;
+
+	/* strip leading punctuation */
+	p = strchr(s, '*');
+	if(p == nil)
+		p = s;
+	while(*p && !isalpha(*p))
+		p++;
+	if(strlen(p) < 2)
+{
+		return -1;
+}
+	memmove(s, p, strlen(p)+1);
+
+	/* strip suffix of punctuation */
+	p = s+strlen(s);
+	op = p;
+	while(p > s && (uchar)p[-1]<0x80 && !isalpha(p[-1]))
+		p--;
+
+	/* chop punctuation */
+	if(p > s){
+		/* free!!! -> free! */
+		if(p+1 < op){
+			p[1] = 0;
+			return 0;
+		}
+		/* free! -> free */
+		if(p < op){
+			p[0] = 0;
+			return 0;
+		}
+	}
+
+	mix = mix1 = 0;
+	if(isupper(s[0]))
+		mix = 1;
+	for(p=s+1; *p; p++)
+		if(isupper(*p)){
+			mix1 = 1;
+			break;
+		}
+
+	/* turn FREE into Free */
+	if(mix1){
+		for(p=s+1; *p; p++)
+			if(isupper(*p))
+				*p += 'a'-'A';
+		return 0;
+	}
+
+	/* turn Free into free */
+	if(mix){
+		*s += 'a'-'A';
+		return 0;
+	}
+	return -1;
+}		
+
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/regcomp.c
@@ -1,0 +1,563 @@
+/* From libregexp but leaks extra classes when it runs out */
+
+
+#include <u.h>
+#include <libc.h>
+#include "regexp.h"
+#include "regcomp.h"
+
+#define	TRUE	1
+#define	FALSE	0
+
+/*
+ * Parser Information
+ */
+typedef
+struct Node
+{
+	Reinst*	first;
+	Reinst*	last;
+}Node;
+
+#define	NSTACK	20
+static	Node	andstack[NSTACK];
+static	Node	*andp;
+static	int	atorstack[NSTACK];
+static	int*	atorp;
+static	int	cursubid;		/* id of current subexpression */
+static	int	subidstack[NSTACK];	/* parallel to atorstack */
+static	int*	subidp;
+static	int	lastwasand;	/* Last token was operand */
+static	int	nbra;
+static	char*	exprp;		/* pointer to next character in source expression */
+static	int	lexdone;
+static	int	nclass;
+static	Reclass*classp;
+static	Reinst*	freep;
+static	int	errors;
+static	Rune	yyrune;		/* last lex'd rune */
+static	Reclass*yyclassp;	/* last lex'd class */
+
+/* predeclared crap */
+static	void	operator(int);
+static	void	pushand(Reinst*, Reinst*);
+static	void	pushator(int);
+static	void	evaluntil(int);
+static	int	bldcclass(void);
+
+static jmp_buf regkaboom;
+
+static	void
+rcerror(char *s)
+{
+	errors++;
+	regerror(s);
+	longjmp(regkaboom, 1);
+}
+
+static	Reinst*
+newinst(int t)
+{
+	freep->type = t;
+	freep->left = 0;
+	freep->right = 0;
+	return freep++;
+}
+
+static	void
+operand(int t)
+{
+	Reinst *i;
+
+	if(lastwasand)
+		operator(CAT);	/* catenate is implicit */
+	i = newinst(t);
+
+	if(t == CCLASS || t == NCCLASS)
+		i->cp = yyclassp;
+	if(t == RUNE)
+		i->r = yyrune;
+
+	pushand(i, i);
+	lastwasand = TRUE;
+}
+
+static	void
+operator(int t)
+{
+	if(t==RBRA && --nbra<0)
+		rcerror("unmatched right paren");
+	if(t==LBRA){
+		if(++cursubid >= NSUBEXP)
+			rcerror ("too many subexpressions");
+		nbra++;
+		if(lastwasand)
+			operator(CAT);
+	} else
+		evaluntil(t);
+	if(t != RBRA)
+		pushator(t);
+	lastwasand = FALSE;
+	if(t==STAR || t==QUEST || t==PLUS || t==RBRA)
+		lastwasand = TRUE;	/* these look like operands */
+}
+
+static	void
+regerr2(char *s, int c)
+{
+	char buf[100];
+	char *cp = buf;
+	while(*s)
+		*cp++ = *s++;
+	*cp++ = c;
+	*cp = '\0'; 
+	rcerror(buf);
+}
+
+static	void
+cant(char *s)
+{
+	char buf[100];
+	strcpy(buf, "can't happen: ");
+	strcat(buf, s);
+	rcerror(buf);
+}
+
+static	void
+pushand(Reinst *f, Reinst *l)
+{
+	if(andp >= &andstack[NSTACK])
+		cant("operand stack overflow");
+	andp->first = f;
+	andp->last = l;
+	andp++;
+}
+
+static	void
+pushator(int t)
+{
+	if(atorp >= &atorstack[NSTACK])
+		cant("operator stack overflow");
+	*atorp++ = t;
+	*subidp++ = cursubid;
+}
+
+static	Node*
+popand(int op)
+{
+	Reinst *inst;
+
+	if(andp <= &andstack[0]){
+		regerr2("missing operand for ", op);
+		inst = newinst(NOP);
+		pushand(inst,inst);
+	}
+	return --andp;
+}
+
+static	int
+popator(void)
+{
+	if(atorp <= &atorstack[0])
+		cant("operator stack underflow");
+	--subidp;
+	return *--atorp;
+}
+
+static	void
+evaluntil(int pri)
+{
+	Node *op1, *op2;
+	Reinst *inst1, *inst2;
+
+	while(pri==RBRA || atorp[-1]>=pri){
+		switch(popator()){
+		default:
+			rcerror("unknown operator in evaluntil");
+			break;
+		case LBRA:		/* must have been RBRA */
+			op1 = popand('(');
+			inst2 = newinst(RBRA);
+			inst2->subid = *subidp;
+			op1->last->next = inst2;
+			inst1 = newinst(LBRA);
+			inst1->subid = *subidp;
+			inst1->next = op1->first;
+			pushand(inst1, inst2);
+			return;
+		case OR:
+			op2 = popand('|');
+			op1 = popand('|');
+			inst2 = newinst(NOP);
+			op2->last->next = inst2;
+			op1->last->next = inst2;
+			inst1 = newinst(OR);
+			inst1->right = op1->first;
+			inst1->left = op2->first;
+			pushand(inst1, inst2);
+			break;
+		case CAT:
+			op2 = popand(0);
+			op1 = popand(0);
+			op1->last->next = op2->first;
+			pushand(op1->first, op2->last);
+			break;
+		case STAR:
+			op2 = popand('*');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(inst1, inst1);
+			break;
+		case PLUS:
+			op2 = popand('+');
+			inst1 = newinst(OR);
+			op2->last->next = inst1;
+			inst1->right = op2->first;
+			pushand(op2->first, inst1);
+			break;
+		case QUEST:
+			op2 = popand('?');
+			inst1 = newinst(OR);
+			inst2 = newinst(NOP);
+			inst1->left = inst2;
+			inst1->right = op2->first;
+			op2->last->next = inst2;
+			pushand(inst1, inst2);
+			break;
+		}
+	}
+}
+
+static	Reprog*
+optimize(Reprog *pp)
+{
+	Reinst *inst, *target;
+	int size;
+	Reprog *npp;
+	Reclass *cl;
+	int diff;
+
+	/*
+	 *  get rid of NOOP chains
+	 */
+	for(inst=pp->firstinst; inst->type!=END; inst++){
+		target = inst->next;
+		while(target->type == NOP)
+			target = target->next;
+		inst->next = target;
+	}
+
+	/*
+	 *  The original allocation is for an area larger than
+	 *  necessary.  Reallocate to the actual space used
+	 *  and then relocate the code.
+	 */
+	size = sizeof(Reprog) + (freep - pp->firstinst)*sizeof(Reinst);
+	npp = realloc(pp, size);
+	if(npp==0 || npp==pp)
+		return pp;
+	diff = (char *)npp - (char *)pp;
+	freep = (Reinst *)((char *)freep + diff);
+	for(inst=npp->firstinst; inst<freep; inst++){
+		switch(inst->type){
+		case OR:
+		case STAR:
+		case PLUS:
+		case QUEST:
+			*(char **)&inst->right += diff;
+			break;
+		case CCLASS:
+		case NCCLASS:
+			*(char **)&inst->right += diff;
+			cl = inst->cp;
+			*(char **)&cl->end += diff;
+			break;
+		}
+		*(char **)&inst->left += diff;
+	}
+	*(char **)&npp->startinst += diff;
+	return npp;
+}
+
+#ifdef	DEBUG
+static	void
+dumpstack(void){
+	Node *stk;
+	int *ip;
+
+	print("operators\n");
+	for(ip=atorstack; ip<atorp; ip++)
+		print("0%o\n", *ip);
+	print("operands\n");
+	for(stk=andstack; stk<andp; stk++)
+		print("0%o\t0%o\n", stk->first->type, stk->last->type);
+}
+
+static	void
+dump(Reprog *pp)
+{
+	Reinst *l;
+	Rune *p;
+
+	l = pp->firstinst;
+	do{
+		print("%d:\t0%o\t%d\t%d", l-pp->firstinst, l->type,
+			l->left-pp->firstinst, l->right-pp->firstinst);
+		if(l->type == RUNE)
+			print("\t%C\n", l->r);
+		else if(l->type == CCLASS || l->type == NCCLASS){
+			print("\t[");
+			if(l->type == NCCLASS)
+				print("^");
+			for(p = l->cp->spans; p < l->cp->end; p += 2)
+				if(p[0] == p[1])
+					print("%C", p[0]);
+				else
+					print("%C-%C", p[0], p[1]);
+			print("]\n");
+		} else
+			print("\n");
+	}while(l++->type);
+}
+#endif
+
+static	Reclass*
+newclass(void)
+{
+	if(nclass <= 0){
+		classp = mallocz(128*sizeof(Reclass), 1);
+		if(classp == nil)
+			regerror("out of memory");
+		nclass = 128;
+	}
+	return &classp[--nclass];
+}
+
+static	int
+nextc(Rune *rp)
+{
+	if(lexdone){
+		*rp = 0;
+		return 1;
+	}
+	exprp += chartorune(rp, exprp);
+	if(*rp == L'\\'){
+		exprp += chartorune(rp, exprp);
+		return 1;
+	}
+	if(*rp == 0)
+		lexdone = 1;
+	return 0;
+}
+
+static	int
+lex(int literal, int dot_type)
+{
+	int quoted;
+
+	quoted = nextc(&yyrune);
+	if(literal || quoted){
+		if(yyrune == 0)
+			return END;
+		return RUNE;
+	}
+
+	switch(yyrune){
+	case 0:
+		return END;
+	case L'*':
+		return STAR;
+	case L'?':
+		return QUEST;
+	case L'+':
+		return PLUS;
+	case L'|':
+		return OR;
+	case L'.':
+		return dot_type;
+	case L'(':
+		return LBRA;
+	case L')':
+		return RBRA;
+	case L'^':
+		return BOL;
+	case L'$':
+		return EOL;
+	case L'[':
+		return bldcclass();
+	}
+	return RUNE;
+}
+
+static int
+bldcclass(void)
+{
+	int type;
+	Rune r[NCCRUNE];
+	Rune *p, *ep, *np;
+	Rune rune;
+	int quoted;
+
+	/* we have already seen the '[' */
+	type = CCLASS;
+	yyclassp = newclass();
+
+	/* look ahead for negation */
+	/* SPECIAL CASE!!! negated classes don't match \n */
+	ep = r;
+	quoted = nextc(&rune);
+	if(!quoted && rune == L'^'){
+		type = NCCLASS;
+		quoted = nextc(&rune);
+		*ep++ = L'\n';
+		*ep++ = L'\n';
+	}
+
+	/* parse class into a set of spans */
+	for(; ep<&r[NCCRUNE];){
+		if(rune == 0){
+			rcerror("malformed '[]'");
+			return 0;
+		}
+		if(!quoted && rune == L']')
+			break;
+		if(!quoted && rune == L'-'){
+			if(ep == r){
+				rcerror("malformed '[]'");
+				return 0;
+			}
+			quoted = nextc(&rune);
+			if((!quoted && rune == L']') || rune == 0){
+				rcerror("malformed '[]'");
+				return 0;
+			}
+			*(ep-1) = rune;
+		} else {
+			*ep++ = rune;
+			*ep++ = rune;
+		}
+		quoted = nextc(&rune);
+	}
+
+	/* sort on span start */
+	for(p = r; p < ep; p += 2){
+		for(np = p; np < ep; np += 2)
+			if(*np < *p){
+				rune = np[0];
+				np[0] = p[0];
+				p[0] = rune;
+				rune = np[1];
+				np[1] = p[1];
+				p[1] = rune;
+			}
+	}
+
+	/* merge spans */
+	np = yyclassp->spans;
+	p = r;
+	if(r == ep)
+		yyclassp->end = np;
+	else {
+		np[0] = *p++;
+		np[1] = *p++;
+		for(; p < ep; p += 2)
+			if(p[0] <= np[1]){
+				if(p[1] > np[1])
+					np[1] = p[1];
+			} else {
+				np += 2;
+				np[0] = p[0];
+				np[1] = p[1];
+			}
+		yyclassp->end = np+2;
+	}
+
+	return type;
+}
+
+static	Reprog*
+regcomp1(char *s, int literal, int dot_type)
+{
+	int token;
+	Reprog *pp;
+
+	/* get memory for the program */
+	pp = malloc(sizeof(Reprog) + 6*sizeof(Reinst)*strlen(s));
+	if(pp == 0){
+		regerror("out of memory");
+		return 0;
+	}
+	freep = pp->firstinst;
+	classp = pp->class;
+	errors = 0;
+
+	if(setjmp(regkaboom))
+		goto out;
+
+	/* go compile the sucker */
+	lexdone = 0;
+	exprp = s;
+	nclass = NCLASS;
+	nbra = 0;
+	atorp = atorstack;
+	andp = andstack;
+	subidp = subidstack;
+	lastwasand = FALSE;
+	cursubid = 0;
+
+	/* Start with a low priority operator to prime parser */
+	pushator(START-1);
+	while((token = lex(literal, dot_type)) != END){
+		if((token&0300) == OPERATOR)
+			operator(token);
+		else
+			operand(token);
+	}
+
+	/* Close with a low priority operator */
+	evaluntil(START);
+
+	/* Force END */
+	operand(END);
+	evaluntil(START);
+#ifdef DEBUG
+	dumpstack();
+#endif
+	if(nbra)
+		rcerror("unmatched left paren");
+	--andp;	/* points to first and only operand */
+	pp->startinst = andp->first;
+#ifdef DEBUG
+	dump(pp);
+#endif
+	pp = optimize(pp);
+#ifdef DEBUG
+	print("start: %d\n", andp->first-pp->firstinst);
+	dump(pp);
+#endif
+out:
+	if(errors){
+		free(pp);
+		pp = 0;
+	}
+	return pp;
+}
+
+extern	Reprog*
+regcomp(char *s)
+{
+	return regcomp1(s, 0, ANY);
+}
+
+extern	Reprog*
+regcomplit(char *s)
+{
+	return regcomp1(s, 1, ANY);
+}
+
+extern	Reprog*
+regcompnl(char *s)
+{
+	return regcomp1(s, 0, ANYNL);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/regcomp.h
@@ -1,0 +1,63 @@
+/*
+ *  substitution list
+ */
+#define NSUBEXP 32
+typedef struct Resublist	Resublist;
+struct	Resublist
+{
+	Resub	m[NSUBEXP];
+};
+
+/*
+ * Actions and Tokens (Reinst types)
+ *
+ *	02xx are operators, value == precedence
+ *	03xx are tokens, i.e. operands for operators
+ */
+#define RUNE		0177
+#define	OPERATOR	0200	/* Bitmask of all operators */
+#define	START		0200	/* Start, used for marker on stack */
+#define	RBRA		0201	/* Right bracket, ) */
+#define	LBRA		0202	/* Left bracket, ( */
+#define	OR		0203	/* Alternation, | */
+#define	CAT		0204	/* Concatentation, implicit operator */
+#define	STAR		0205	/* Closure, * */
+#define	PLUS		0206	/* a+ == aa* */
+#define	QUEST		0207	/* a? == a|nothing, i.e. 0 or 1 a's */
+#define	ANY		0300	/* Any character except newline, . */
+#define	ANYNL		0301	/* Any character including newline, . */
+#define	NOP		0302	/* No operation, internal use only */
+#define	BOL		0303	/* Beginning of line, ^ */
+#define	EOL		0304	/* End of line, $ */
+#define	CCLASS		0305	/* Character class, [] */
+#define	NCCLASS		0306	/* Negated character class, [] */
+#define	END		0377	/* Terminate: match found */
+
+/*
+ *  regexec execution lists
+ */
+#define LISTSIZE	10
+#define BIGLISTSIZE	(25*LISTSIZE)
+typedef struct Relist	Relist;
+struct Relist
+{
+	Reinst*		inst;		/* Reinstruction of the thread */
+	Resublist	se;		/* matched subexpressions in this thread */
+};
+typedef struct Reljunk	Reljunk;
+struct	Reljunk
+{
+	Relist*	relist[2];
+	Relist*	reliste[2];
+	int	starttype;
+	Rune	startchar;
+	char*	starts;
+	char*	eol;
+	Rune*	rstarts;
+	Rune*	reol;
+};
+
+extern Relist*	_renewthread(Relist*, Reinst*, int, Resublist*);
+extern void	_renewmatch(Resub*, int, Resublist*);
+extern Relist*	_renewemptythread(Relist*, Reinst*, int, char*);
+extern Relist*	_rrenewemptythread(Relist*, Reinst*, int, Rune*);
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/regen.c
@@ -1,0 +1,176 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include "regexp.h"
+#include "dfa.h"
+
+/***
+ * Regular expression for matching.
+ */
+
+char *ignore[] = 
+{
+	/* HTML that isn't A, IMG, or FONT */
+	/* Must have a space somewhere to avoid catching <email@address> */
+	"<[ 	\n\r]*("
+		"[^aif]|"
+		"a[^> \t\r\n]|"
+		"i[^mM \t\r\n]|"
+		"im[^gG \t\r\n]|"
+		"img[^> \t\r\n]|"
+		"f[^oO \t\r\n]|"
+		"fo[^Nn \t\r\n]|"
+		"fon[^tT \t\r\n]|"
+		"font[^> \r\t\n]"
+	")[^>]*[ \t\n\r][^>]*>",
+	"<[ 	\n\r]*("
+		"i|im|f|fo|fon"
+	")[ \t\r\n][^>]*>",
+
+	/* ignore html comments */
+	"<!--([^\\-]|-[^\\-]|--[^>]|\n)*-->",
+
+	/* random mail strings */
+	"^message-id:.*\n([ 	].*\n)*",
+	"^in-reply-to:.*\n([ 	].*\n)*",
+	"^references:.*\n([ 	].*\n)*",
+	"^date:.*\n([ 	].*\n)*",
+	"^delivery-date:.*\n([ 	].*\n)*",
+	"e?smtp id .*",
+	"^	id.*",
+	"boundary=.*",
+	"name=\"",
+	"filename=\"",
+	"news:<[^>]+>",
+	"^--[^ 	]*$",
+
+	/* base64 encoding */
+	"^[0-9a-zA-Z+\\-=/]+$",
+
+	/* uu encoding */
+	"^[!-Z]+$",
+
+	/* little things */
+	".",
+	"\n",
+};
+
+char *keywords[] =
+{
+	"([a-zA-Z'`$!¡-￿]|[0-9]([.,][0-9])*)+",
+};
+
+int debug;
+
+Dreprog*
+dregcomp(char *buf)
+{
+	Reprog *r;
+	Dreprog *d;
+
+	if(debug)
+		print(">>> '%s'\n", buf);
+
+	r = regcomp(buf);
+	if(r == nil)
+		sysfatal("regcomp");
+	d = dregcvt(r);
+	if(d == nil)
+		sysfatal("dregcomp");
+	free(r);
+	return d;
+}
+
+char*
+strcpycase(char *d, char *s)
+{
+	int cc, esc;
+
+	cc = 0;
+	esc = 0;
+	while(*s){
+		if(*s == '[')
+			cc++;
+		if(*s == ']')
+			cc--;
+		if(!cc && 'a' <= *s && *s <= 'z'){
+			*d++ = '[';
+			*d++ = *s;
+			*d++ = *s+'A'-'a';
+			*d++ = ']';
+		}else
+			*d++ = *s;
+		if(*s == '\\')
+			esc++;
+		else if(esc)
+			esc--;
+		s++;
+	}
+	return d;
+}
+
+void
+regerror(char *msg)
+{
+	sysfatal("regerror: %s", msg);
+}
+
+void
+buildre(Dreprog *re[3])
+{
+	int i;
+	static char buf[16384], *s;
+
+	re[0] = dregcomp("^From ");
+	
+	s = buf;
+	for(i=0; i<nelem(keywords); i++){
+		if(i != 0)
+			*s++ = '|';
+		s = strcpycase(s, keywords[i]);
+	}
+	*s = 0;
+	re[1] = dregcomp(buf);
+
+	s = buf;
+	for(i=0; i<nelem(ignore); i++){
+		if(i != 0)
+			*s++ = '|';
+		s = strcpycase(s, ignore[i]);
+	}
+	*s = 0;
+	re[2] = dregcomp(buf);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: regen [-d]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	Dreprog *re[3];
+	Biobuf b;
+
+	ARGBEGIN{
+	default:
+		usage();
+	case 'd':
+		debug = 1;
+	}ARGEND
+
+	if(argc != 0)
+		usage();
+
+	buildre(re);
+	Binit(&b, 1, OWRITE);
+	Bprintdfa(&b, re[0]);
+	Bprintdfa(&b, re[1]);
+	Bprintdfa(&b, re[2]);
+	exits(0);
+}
+
+	
\ No newline at end of file
--- /dev/null
+++ b/sys/src/cmd/upas/bayes/regexp.h
@@ -1,0 +1,63 @@
+typedef struct Resub		Resub;
+typedef struct Reclass		Reclass;
+typedef struct Reinst		Reinst;
+typedef struct Reprog		Reprog;
+
+/*
+ *	Sub expression matches
+ */
+struct Resub{
+	union
+	{
+		char *sp;
+		Rune *rsp;
+	};
+	union
+	{
+		char *ep;
+		Rune *rep;
+	};
+};
+
+/*
+ *	character class, each pair of rune's defines a range
+ */
+struct Reclass{
+	Rune	*end;
+	Rune	spans[64];
+};
+
+/*
+ *	Machine instructions
+ */
+struct Reinst{
+	int	type;
+	union	{
+		Reclass	*cp;		/* class pointer */
+		Rune	r;		/* character */
+		int	subid;		/* sub-expression id for RBRA and LBRA */
+		Reinst	*right;		/* right child of OR */
+	};
+	union {	/* regexp relies on these two being in the same union */
+		Reinst *left;		/* left child of OR */
+		Reinst *next;		/* next instruction for CAT & LBRA */
+	};
+};
+
+/*
+ *	Reprogram definition
+ */
+struct Reprog{
+	Reinst	*startinst;	/* start pc */
+	Reclass	class[16];	/* .data */
+	Reinst	firstinst[5];	/* .text */
+};
+
+extern Reprog	*regcomp(char*);
+extern Reprog	*regcomplit(char*);
+extern Reprog	*regcompnl(char*);
+extern void	regerror(char*);
+extern int	regexec(Reprog*, char*, Resub*, int);
+extern void	regsub(char*, char*, int, Resub*, int);
+extern int	rregexec(Reprog*, Rune*, Resub*, int);
+extern void	rregsub(Rune*, Rune*, int, Resub*, int);
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/isspam.rc
@@ -1,0 +1,2 @@
+#!/bin/rc
+exec /mail/lib/isspam.rc $*
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/mkfile
@@ -1,0 +1,33 @@
+</$objtype/mkfile
+
+RCFILES=\
+	isspam\
+	msgcat\
+	spam\
+	tfmt\
+	unspam\
+
+all:Q:
+	;
+
+installall:Q:	install
+	;
+
+install:V: ${RCFILES:%=$BIN/%}
+
+safeinstall:V: install
+
+safeinstallall:V: install
+
+clean:Q:
+	;
+nuke:V:
+	rm -f $BIN/^($RCFILES)
+
+$BIN/%: %.rc
+	cp $stem.rc $BIN/$stem
+
+test:VQ:
+	# nothing
+
+<../mkupas
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/mkfile.rc
@@ -1,0 +1,22 @@
+
+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 -f /rc/bin/mail
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/msgcat.rc
@@ -1,0 +1,11 @@
+#!/bin/rc
+
+f=$*
+if(~ $#f 0)
+	f=/mail/fs/mbox/[0-9]*
+f=`{echo $f|sed s:/mail/fs/mbox/::g}
+
+{
+	for(i in $f)
+		echo $i p
+} | upas/nedmail >[2=]
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/spam.rc
@@ -1,0 +1,2 @@
+#!/bin/rc
+exec /mail/lib/spam.rc $*
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/tfmt.rc
@@ -1,0 +1,25 @@
+#!/bin/rc
+# anti-topposting defense
+
+# sed '/^[ 	]*>[ 	]*>[ 	]*>/q'
+
+awk '
+{
+	if(l[i] ~ /^[ 	]*>[ 	]*>[ 	]*>/)
+		q = 1
+	if(q == 0)
+		l[i = NR] = $0;
+}
+END{
+	for(; i > 1; i--)
+		if(l[i] !~ /^([ 	]*>)*[ 	]*$/)
+			break;
+	for(; i > 1; i--)
+		if(l[i] !~ /^[ 	]*>[ 	]*>/)
+			break;
+	for(; i > 1; i--)
+		if(l[i] !~ /^([ 	]*>)*[ 	]*$/)
+			break;
+	for(j = 1; j <= i; j++)
+		print l[j]
+}' |dd -conv block >[2=]
--- /dev/null
+++ b/sys/src/cmd/upas/binscripts/unspam.rc
@@ -1,0 +1,2 @@
+#!/bin/rc
+exec /mail/lib/unspam.rc $*
--- /dev/null
+++ b/sys/src/cmd/upas/common/aux.c
@@ -1,0 +1,119 @@
+#include "common.h"
+
+/*
+ *  check for shell characters in a String
+ */
+static char *illegalchars = "\r\n";
+
+extern int
+shellchars(char *cp)
+{
+	char *sp;
+
+	for(sp=illegalchars; *sp; sp++)
+		if(strchr(cp, *sp))
+			return 1;
+	return 0;
+}
+
+static char *specialchars = " ()<>{};=\\'\`^&|";
+static char *escape = "%%";
+
+int
+hexchar(int x)
+{
+	x &= 0xf;
+	if(x < 10)
+		return '0' + x;
+	else
+		return 'A' + x - 10;
+}
+
+/*
+ *  rewrite a string to escape shell characters
+ */
+extern String*
+escapespecial(String *s)
+{
+	String *ns;
+	char *sp;
+
+	for(sp = specialchars; *sp; sp++)
+		if(strchr(s_to_c(s), *sp))
+			break;
+	if(*sp == 0)
+		return s;
+
+	ns = s_new();
+	for(sp = s_to_c(s); *sp; sp++){
+		if(strchr(specialchars, *sp)){
+			s_append(ns, escape);
+			s_putc(ns, hexchar(*sp>>4));
+			s_putc(ns, hexchar(*sp));
+		} else
+			s_putc(ns, *sp);
+	}
+	s_terminate(ns);
+	s_free(s);
+	return ns;
+}
+
+uint
+hex2uint(char 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 -512;
+}
+
+/*
+ *  rewrite a string to remove shell characters escapes
+ */
+extern String*
+unescapespecial(String *s)
+{
+	char *sp;
+	uint c, n;
+	String *ns;
+
+	if(strstr(s_to_c(s), escape) == 0)
+		return s;
+	n = strlen(escape);
+
+	ns = s_new();
+	for(sp = s_to_c(s); *sp; sp++){
+		if(strncmp(sp, escape, n) == 0){
+			c = (hex2uint(sp[n])<<4) | hex2uint(sp[n+1]);
+			if(c & 0x80)
+				s_putc(ns, *sp);
+			else {
+				s_putc(ns, c);
+				sp += n+2-1;
+			}
+		} else
+			s_putc(ns, *sp);
+	}
+	s_terminate(ns);
+	s_free(s);
+	return ns;
+
+}
+
+int
+returnable(char *path)
+{
+	return strcmp(path, "/dev/null") != 0;
+}
+
+int
+temperror(void)
+{
+	char err[ERRMAX];
+
+	rerrstr(err, sizeof(err));
+	return strstr(err, "too much activity") != nil || strstr(err, "temporary problem") != nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/common/become.c
@@ -1,0 +1,23 @@
+#include "common.h"
+#include <auth.h>
+#include <ndb.h>
+
+/*
+ *  become powerless user
+ */
+int
+become(char **, char *who)
+{
+	if(strcmp(who, "none") == 0) {
+		if(procsetuser("none") < 0) {
+			werrstr("can't become none: %r");
+			return -1;
+		}
+		if(newns("none", nil) < 0) {
+			werrstr("can't set new namespace: %r");
+			return -1;
+		}
+	}
+	return 0;
+}
+
--- /dev/null
+++ b/sys/src/cmd/upas/common/common.h
@@ -1,0 +1,78 @@
+enum
+{
+	Elemlen	= 56,
+	Pathlen	= 256,
+};
+
+#include "sys.h"
+#include <String.h>
+
+enum{
+	Fields	= 18,
+
+	/* flags */
+	Fanswered	= 1<<0, /* a */
+	Fdeleted		= 1<<1, /* D */
+	Fdraft		= 1<<2, /* d */
+	Fflagged		= 1<<3, /* f */
+	Frecent		= 1<<4, /* r	we are the first fs to see this */
+	Fseen		= 1<<5, /* s */
+	Fstored		= 1<<6, /* S */
+	Nflags		= 7,
+};
+#define Timefmt "WW MMM _D hh:mm:ss ?Z YYYY"
+
+/*
+ * flag.c
+ */
+char	*flagbuf(char*, int);
+int	buftoflags(char*);
+char	*txflags(char*, uchar*);
+
+/*
+ *  routines in aux.c
+ */
+char	*mboxpathbuf(char*, int, char*, char*);
+char	*basename(char*);
+int	shellchars(char*);
+String	*escapespecial(String*);
+String	*unescapespecial(String*);
+int	returnable(char*);
+int	temperror(void);
+
+/* folder.c */
+Biobuf	*openfolder(char*, long);
+int	closefolder(Biobuf*);
+int	appendfolder(Biobuf*, char*, int);
+int	fappendfolder(char*, long, char *, int);
+int	fappendfile(char*, char*, int);
+char*	foldername(char*, char*, char*);
+char*	ffoldername(char*, char*, char*);
+
+/* fmt.c */
+void	mailfmtinstall(void);	/* 'U' = 2047fmt */
+#pragma varargck	type	"U"	char*
+
+/* a pipe between parent and child*/
+typedef struct{
+	Biobuf	bb;
+	Biobuf	*fp;	/* parent process end*/
+	int	fd;	/* child process end*/
+} stream;
+
+/* a child process*/
+typedef struct{
+	stream	*std[3];	/* standard fd's*/
+	int	pid;		/* process identifier*/
+	int	status;		/* exit status*/
+	Waitmsg	*waitmsg;
+} process;
+
+stream	*instream(void);
+stream	*outstream(void);
+void	stream_free(stream*);
+process	*noshell_proc_start(char**, stream*, stream*, stream*, int, char*);
+process	*proc_start(char*, stream*, stream*, stream*, int, char*);
+int	proc_wait(process*);
+int	proc_free(process*);
+//int	proc_kill(process*);
--- /dev/null
+++ b/sys/src/cmd/upas/common/config.c
@@ -1,0 +1,9 @@
+#include "common.h"
+
+char *MAILROOT = "/mail";
+char *SPOOL	= "/mail";
+char *UPASLOG	= "/sys/log";
+char *UPASLIB	= "/mail/lib";
+char *UPASBIN	= "/bin/upas";
+char *UPASTMP	= "/mail/tmp";
+char *SHELL	= "/bin/rc";
--- /dev/null
+++ b/sys/src/cmd/upas/common/flags.c
@@ -1,0 +1,73 @@
+#include "common.h"
+
+static uchar flagtab[] = {
+	'a',	Fanswered,
+	'D',	Fdeleted,
+	'd',	Fdraft,
+	'f',	Fflagged,
+	'r',	Frecent,
+	's',	Fseen,
+	'S',	Fstored,
+};
+
+char*
+flagbuf(char *buf, int flags)
+{
+	char *p, c;
+	int i;
+
+	p = buf;
+	for(i = 0; i < nelem(flagtab); i += 2){
+		c = '-';
+		if(flags & flagtab[i+1])
+			c = flagtab[i];
+		*p++ = c;
+	}
+	*p = 0;
+	return buf;
+}
+
+int
+buftoflags(char *p)
+{
+	uchar flags;
+	int i;
+
+	flags = 0;
+	for(i = 0; i < nelem(flagtab); i += 2)
+		if(p[i>>1] == flagtab[i])
+			flags |= flagtab[i + 1];
+	return flags;
+}
+
+char*
+txflags(char *p, uchar *flags)
+{
+	uchar neg, f, c, i;
+
+	for(;;){
+		neg = 0;
+	again:
+		if((c = *p++) == '-'){
+			neg = 1;
+			goto again;
+		}else if(c == '+'){
+			neg = 0;
+			goto again;	
+		}
+		if(c == 0)
+			return nil;
+		for(i = 0;; i += 2){
+			if(i == nelem(flagtab))
+				return "bad flag";
+			if(c == flagtab[i]){
+				f = flagtab[i+1];
+				break;
+			}
+		}
+		if(neg)
+			*flags &= ~f;
+		else
+			*flags |= f;
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/common/fmt.c
@@ -1,0 +1,35 @@
+#include "common.h"
+
+int
+rfc2047fmt(Fmt *fmt)
+{
+	char *s, *p;
+
+	s = va_arg(fmt->args, char*);
+	if(s == nil)
+		return fmtstrcpy(fmt, "");
+	for(p=s; *p; p++)
+		if((uchar)*p >= 0x80)
+			goto hard;
+	return fmtstrcpy(fmt, s);
+
+hard:
+	fmtprint(fmt, "=?utf-8?q?");
+	for(p = s; *p; p++){
+		if(*p == ' ')
+			fmtrune(fmt, '_');
+		else if(*p == '_' || *p == '\t' || *p == '=' || *p == '?' ||
+		    (uchar)*p >= 0x80)
+			fmtprint(fmt, "=%.2uX", (uchar)*p);
+		else
+			fmtrune(fmt, (uchar)*p);
+	}
+	fmtprint(fmt, "?=");
+	return 0;
+}
+
+void
+mailfmtinstall(void)
+{
+	fmtinstall('U', rfc2047fmt);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/common/folder.c
@@ -1,0 +1,375 @@
+#include "common.h"
+
+enum{
+	Mbox	= 1,
+	Mdir,
+};
+
+typedef struct Folder Folder;
+struct Folder{
+	int	open;
+	int	ofd;
+	int	type;
+	Biobuf	*out;
+	Mlock	*l;
+	long	t;
+};
+static Folder ftab[5];
+
+static Folder*
+getfolder(Biobuf *out)
+{
+	int i;
+	Folder *f;
+
+	for(i = 0; i < nelem(ftab); i++){
+		f = ftab+i;
+		if(f->open == 0){
+			f->open = 1;
+			f->ofd = -1;
+			f->type = 0;
+			return f;
+		}
+		if(f->out == out)
+			return f;
+	}
+	sysfatal("folder.c:ftab too small");
+	return 0;
+}
+
+static int
+putfolder(Folder *f)
+{
+	int r;
+
+	r = 0;
+	if(f->l)
+		sysunlock(f->l);
+	if(f->out){
+		r |= Bterm(f->out);
+		free(f->out);
+	}
+	if(f->ofd >= 0)
+		close(f->ofd);
+	memset(f, 0, sizeof *f);
+	return r;
+}
+
+static Biobuf*
+mboxopen(char *s)
+{
+	Folder *f;
+
+	f = getfolder(nil);
+	f->l = syslock(s);		/* traditional botch: ignore failure */
+	if((f->ofd = open(s, OWRITE)) == -1)
+	if((f->ofd = create(s, OWRITE|OEXCL, DMAPPEND|0600)) == -1){
+		putfolder(f);
+		return nil;
+	}
+	seek(f->ofd, 0, 2);
+	f->out = malloc(sizeof *f->out);
+	Binit(f->out, f->ofd, OWRITE);
+	f->type = Mbox;
+	return f->out;
+}
+
+/*
+ * sync with send/cat_mail.c:/^mdir
+ */
+static Biobuf*
+mdiropen(char *s, long t)
+{
+	char buf[Pathlen];
+	Folder *f;
+	int i;
+
+	f = getfolder(nil);
+	for(i = 0; i < 100; i++){
+		snprint(buf, sizeof buf, "%s/%lud.%.2d.tmp", s, t, i);
+		if((f->ofd = create(buf, OWRITE|OEXCL, DMAPPEND|0660)) != -1)
+			goto found;
+	}
+	putfolder(f);
+	return nil;
+found:
+	werrstr("");
+	f->out = malloc(sizeof *f->out);
+	Binit(f->out, f->ofd, OWRITE);
+	f->type = Mdir;
+	f->t = t;
+	return f->out;
+}
+
+Biobuf*
+openfolder(char *s, long t)
+{
+	int isdir;
+	Dir *d;
+
+	if(d = dirstat(s)){
+		isdir = d->mode&DMDIR;
+		free(d);
+	}else{
+		isdir = create(s, OREAD, DMDIR|0777);
+		if(isdir == -1)
+			return nil;
+		close(isdir);
+		isdir = 1;
+	}
+	if(isdir)
+		return mdiropen(s, t);
+	else
+		return mboxopen(s);
+}
+
+int
+closefolder(Biobuf *b)
+{
+	char buf[32];
+	Folder *f;
+	Dir d;
+	int i;
+
+	if(b == nil)
+		return 0;
+	f = getfolder(b);
+	if(f->type != Mdir)
+		return putfolder(f);
+	if(Bflush(b) == 0){
+		for(i = 0; i < 100; i++){
+			nulldir(&d);
+			snprint(buf, sizeof buf, "%lud.%.2d", f->t, i);
+			d.name = buf;
+			if(dirfwstat(f->ofd, &d) > 0)
+				return putfolder(f);
+		}
+	}
+	putfolder(f);
+	return -1;
+}
+
+/*
+ * escape "From " at the beginning of a line;
+ * translate \r\n to \n for imap
+ */
+static int
+mboxesc(Biobuf *in, Biobuf *out, int type)
+{
+	char *s;
+	int n;
+
+	for(; s = Brdstr(in, '\n', 0); free(s)){
+		if(!strncmp(s, "From ", 5))
+			Bputc(out, ' ');
+		n = strlen(s);
+		if(n > 1 && s[n-2] == '\r'){
+			s[n-2] = '\n';
+			n--;
+		}
+		if(Bwrite(out, s, n) == Beof){
+			free(s);
+			return -1;
+		}
+		if(s[n-1] != '\n')
+			Bputc(out, '\n');
+	}
+	if(type == Mbox)
+		Bputc(out, '\n');
+	if(Bflush(out) == Beof)
+		return -1;
+	return 0;
+}
+
+int
+appendfolder(Biobuf *b, char *addr, int fd)
+{
+	char *s, *t;
+	int r;
+	Biobuf bin;
+	Folder *f;
+	Tzone *tz;
+	Tm tm;
+
+	f = getfolder(b);
+	Bseek(f->out, 0, 2);
+	Binit(&bin, fd, OREAD);
+
+	s = Brdstr(&bin, '\n', 0);
+
+	/* Unix from */
+	if(s != nil && strncmp(s, "From ", 5) == 0
+	&& (t = strchr(s + 5, ' ')) != nil
+	&& tmparse(&tm, Timefmt, t + 1, nil, nil) != nil){
+		f->t = tmnorm(&tm);
+	}else {
+		/*
+		 * Old mailboxes have dates in ctime format,
+		 * which contains ambiguous timezone names.
+		 * Passing in the local timezone has the side
+		 * effect of disambiguating the timezone name
+		 * as local.
+		 */
+		tz = tzload("local");
+		tmtime(&tm, f->t, tz);
+		Bprint(f->out, "From %s %τ\n", addr, tmfmt(&tm, Timefmt));
+	}
+	if(s != nil)
+		Bwrite(f->out, s, strlen(s));
+	free(s);
+	r = mboxesc(&bin, f->out, f->type);
+	return r | Bterm(&bin);
+}
+
+int
+fappendfolder(char *addr, long t, char *s, int fd)
+{
+	Biobuf *b;
+	int r;
+
+	b = openfolder(s, t);
+	if(b == nil)
+		return -1;
+	r = appendfolder(b, addr, fd);
+	r |= closefolder(b);
+	return r;
+}
+
+/*
+ * BOTCH sync with ../imap4d/mbox.c:/^okmbox
+ */
+
+static char *specialfile[] =
+{
+	"L.mbox",
+	"forward",
+	"headers",
+	"imap.subscribed",
+	"names",
+	"pipefrom",
+	"pipeto",
+};
+
+static int
+special(char *s)
+{
+	char *p;
+	int i;
+
+	p = strrchr(s, '/');
+	if(p == nil)
+		p = s;
+	else
+		p++;
+	for(i = 0; i < nelem(specialfile); i++)
+		if(strcmp(p, specialfile[i]) == 0)
+			return 1;
+	return 0;
+}
+
+static char*
+mkmbpath(char *s, int n, char *user, char *mb, char *path)
+{
+	char *p, *e, *r, buf[Pathlen];
+
+	if(!mb)
+		return mboxpathbuf(s, n, user, path);
+	e = buf+sizeof buf;
+	p = seprint(buf, e, "%s", mb);
+	if(r = strrchr(buf, '/'))
+		p = r;
+	seprint(p, e, "/%s", path);
+	return mboxpathbuf(s, n, user, buf);
+}
+
+
+/*
+ * fancy processing for ned:
+ * we default to storing in $mail/f then just in $mail.
+ */
+char*
+ffoldername(char *mb, char *user, char *rcvr)
+{
+	char *p;
+	int c, n;
+	Dir *d;
+	static char buf[Pathlen];
+
+	d = dirstat(mkmbpath(buf, sizeof buf, user, mb, "f/"));
+	n = strlen(buf);
+	if(!d ||  d->qid.type != QTDIR)
+		buf[n -= 2] = 0;
+	free(d);
+
+	if(p = strrchr(rcvr, '!'))
+		rcvr = p+1;
+	while(n < sizeof buf-1 && (c = *rcvr++)){
+		if(c== '@')
+			break;
+		if(c == '/')
+			c = '_';
+		buf[n++] = c;
+	}
+	buf[n] = 0;
+
+	if(special(buf)){
+		fprint(2, "!won't overwrite %s\n", buf);
+		return nil;
+	}
+	return buf;
+}
+
+char*
+foldername(char *mb, char *user, char *path)
+{
+	static char buf[Pathlen];
+
+	mkmbpath(buf, sizeof buf, user, mb, path);
+	if(special(buf)){
+		fprint(2, "!won't overwrite %s\n", buf);
+		return nil;
+	}
+	return buf;
+}
+
+static int
+append(Biobuf *in, Biobuf *out)
+{
+	char *buf;
+	int n, m;
+
+	buf = malloc(8192);
+	for(;;){
+		m = 0;
+		n = Bread(in, buf, 8192);
+		if(n <= 0)
+			break;
+		m = Bwrite(out, buf, n);
+		if(m != n)
+			break;
+	}
+	if(m != n)
+		n = -1;
+	else
+		n = 1;
+	free(buf);
+	return n;
+}
+
+/* symmetry for nedmail; misnamed */
+int
+fappendfile(char*, char *target, int in)
+{
+	int fd, r;
+	Biobuf bin, out;
+
+	if((fd = create(target, ORDWR|OEXCL, 0666)) == -1)
+		return -1;
+	Binit(&out, fd, OWRITE);
+	Binit(&bin, in, OREAD);
+	r = append(&bin, &out);
+	Bterm(&bin);
+	Bterm(&out);
+	close(fd);
+	return r;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/common/libsys.c
@@ -1,0 +1,706 @@
+#include "common.h"
+#include <auth.h>
+#include <ndb.h>
+
+/*
+ *  return the date
+ */
+Tmfmt
+thedate(Tm *tm)
+{
+	Tzone *tz;
+
+	/* if the local time is screwed, just do gmt */
+	tz = tzload("local");
+	tmnow(tm, tz);
+	return tmfmt(tm, Timefmt);
+}
+
+/*
+ *  return the user id of the current user
+ */
+char *
+getlog(void)
+{
+	return getuser();
+}
+
+/*
+ *  return the lock name (we use one lock per directory)
+ */
+static void
+lockname(Mlock *l, char *path)
+{
+	char *e, *q;
+
+	seprint(l->name, e = l->name+sizeof l->name, "%s", path);
+	q = strrchr(l->name, '/');
+	if(q == nil)
+		q = l->name;
+	else
+		q++;
+	seprint(q, e, "%s", "L.mbox");
+}
+
+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, nd;
+	char *p;
+
+	l->fd = open(l->name, OREAD);
+	if(l->fd >= 0)
+		return 0;
+	if(d = dirstat(l->name)){
+		free(d);
+		return 1;	/* try again later */
+	}
+	l->fd = create(l->name, OREAD, DMEXCL|0666);
+	if(l->fd >= 0){
+		nulldir(&nd);
+		nd.mode = DMEXCL|0666;
+		if(dirfwstat(l->fd, &nd) < 0){
+			/* if we can't chmod, don't bother */
+			/* live without the lock but log it */
+			close(l->fd);
+			l->fd = -1;
+			syslog(0, "mail", "lock error: %s: %r", l->name);
+			remove(l->name);
+		}
+		return 0;
+	}
+	/* couldn't create; let's see what we can whine about */
+	p = strrchr(l->name, '/');
+	if(p != 0){
+		*p = 0;
+		fd = access(l->name, 2);
+		*p = '/';
+	}else
+		fd = access(".", 2);
+	if(fd < 0)
+		/* live without the lock but log it */
+		syslog(0, "mail", "lock error: %s: %r", l->name);
+	close(fd);
+	return 0;
+}
+
+#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.
+ */
+Mlock*
+syslock(char *path)
+{
+	Mlock *l;
+	int tries;
+
+	l = mallocz(sizeof(Mlock), 1);
+	if(l == 0)
+		return nil;
+
+	lockname(l, 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;
+		}
+	free(l);
+	return nil;
+}
+
+/*
+ *  like lock except don't wait
+ */
+Mlock *
+trylock(char *path)
+{
+	char buf[1];
+	int fd;
+	Mlock *l;
+
+	l = mallocz(sizeof(Mlock), 1);
+	if(l == 0)
+		return 0;
+
+	lockname(l, path);
+	if(openlockfile(l) != 0){
+		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(nil);
+	}
+	return l;
+}
+
+void
+syslockrefresh(Mlock *l)
+{
+	char buf[1];
+
+	pread(l->fd, buf, 1, 0);
+}
+
+void
+sysunlock(Mlock *l)
+{
+	if(l == 0)
+		return;
+	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)
+ */
+Biobuf*
+sysopen(char *path, char *mode, ulong perm)
+{
+	int sysperm, sysmode, fd, docreate, append, truncate;
+	Dir *d, nd;
+	Biobuf *bp;
+
+	/*
+	 *  decode the request
+	 */
+	sysperm = 0;
+	sysmode = -1;
+	docreate = 0;
+	append = 0;
+	truncate = 0;
+ 	for(; *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;
+}
+
+/*
+ *  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;
+}
+
+/*
+ *  read in the system name
+ */
+char *
+sysname_read(void)
+{
+	static char name[128];
+	char *s, *c;
+
+	c = s = getenv("site");
+	if(!c)
+		c = alt_sysname_read();
+	if(!c)
+		c = "kremvax";
+	strecpy(name, name+sizeof name, c);
+	free(s);
+	return name;
+}
+
+char *
+alt_sysname_read(void)
+{
+	return sysname();
+}
+
+/*
+ *  get all names
+ */
+char**
+sysnames_read(void)
+{
+	int n;
+	Ndbtuple *t, *nt;
+	static char **namev;
+
+	if(namev)
+		return namev;
+
+	free(csgetvalue(0, "sys", sysname(), "dom", &t));
+
+	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){
+		namev[0] = strdup(sysname_read());
+		namev[1] = strdup(alt_sysname_read());
+		n = 2;
+		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
+ */
+char*
+domainname_read(void)
+{
+	char **p;
+
+	for(p = sysnames_read(); *p; p++)
+		if(strchr(*p, '.'))
+			return *p;
+	return 0;
+}
+
+/*
+ *  rename a file, fails unless both are in the same directory
+ */
+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);
+}
+
+int
+sysexist(char *file)
+{
+	return access(file, AEXIST) == 0;
+}
+
+static char yankeepig[] = "die: yankee pig dog";
+
+int
+syskill(int pid)
+{
+	return postnote(PNPROC, pid, yankeepig);
+}
+
+int
+syskillpg(int pid)
+{
+	return postnote(PNGROUP, pid, yankeepig);
+}
+
+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 *, char *msg)
+{
+	static char *foo = "sys: write on closed pipe";
+
+	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);
+}
+
+int
+islikeatty(int fd)
+{
+	char buf[64];
+	int l;
+
+	if(fd2path(fd, buf, sizeof buf) != 0)
+		return 0;
+
+	/* might be /mnt/term/dev/cons */
+	l = strlen(buf);
+	return l >= 9 && strcmp(buf+l-9, "/dev/cons") == 0;
+}
+
+int
+holdon(void)
+{
+	int fd;
+
+	if(!islikeatty(0))
+		return -1;
+
+	fd = open("/dev/consctl", OWRITE);
+	write(fd, "holdon", 6);
+
+	return fd;
+}
+
+int
+sysopentty(void)
+{
+	return open("/dev/cons", ORDWR);
+}
+
+void
+holdoff(int fd)
+{
+	write(fd, "holdoff", 7);
+	close(fd);
+}
+
+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
+ *
+ */
+char*
+mboxpathbuf(char *to, int n, char *user, char *path)
+{
+	if(*path == '/' || !strncmp(path, "./", 2) || !strncmp(path, "../", 3))
+		snprint(to, n, "%s", path);
+	else
+		snprint(to, n, "%s/box/%s/%s", MAILROOT, user, path);
+	return to;
+}
+
+/*
+ * warning: we're not quoting bad characters.  we're not encoding
+ * non-ascii characters.  basically this function sucks.  don't use.
+ */
+char*
+username0(Biobuf *b, char *from)
+{
+	char *p, *f[6];
+	int n;
+	static char buf[32];
+
+	n = strlen(from);
+	buf[0] = 0;
+	for(;; free(p)) {
+		p = Brdstr(b, '\n', 1);
+		if(p == 0)
+			break;
+		if(strncmp(p, from, n)  || p[n] != '|')
+			continue;
+		if(getfields(p, f, nelem(f), 0, "|") < 3)
+			continue;
+		snprint(buf, sizeof buf, "\"%s\"", f[2]);
+		/* no break; last match wins */
+	}
+	return buf[0]? buf: 0;
+}
+
+char*
+username(char *from)
+{
+	char *s;
+	Biobuf *b;
+
+	s = 0;
+	if(b = Bopen("/adm/keys.who", OREAD)){
+		s = username0(b, from);
+		Bterm(b);
+	}
+	if(s == 0 && (b = Bopen("/adm/netkeys.who", OREAD))){
+		s = username0(b, from);
+		Bterm(b);
+	}
+	return s;
+}
+
+/*
+ * 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;
+}
+
+static int
+createfolder0(char *user, char *folder, char *ftype)
+{
+	char *p, *s, buf[Pathlen];
+	int isdir, mode;
+	Dir *d;
+
+	assert(folder != 0);
+	mboxpathbuf(buf, sizeof buf, user, folder);
+	if(access(buf, 0) == 0){
+		fprint(2, "%s already exists\n", ftype);
+		return -1;
+	}
+	fprint(2, "creating new %s: %s\n", ftype, buf);
+
+	/*
+	 * if we can deliver to this mbox, it needs
+	 * to be read/execable all the way down
+	 */
+	mode = 0711;
+	if(!strncmp(buf, "/mail/box/", 10))
+	if((s = strrchr(buf, '/')) && !strcmp(s+1, "mbox"))
+		mode = 0755;
+	for(p = buf; p; p++) {
+		if(*p == '/')
+			continue;
+		p = strchr(p, '/');
+		if(p == 0)
+			break;
+		*p = 0;
+		if(access(buf, 0) != 0)
+		if(docreate(buf, DMDIR|mode) < 0)
+			return -1;
+		*p = '/';
+	}
+	/* must match folder.c:/^openfolder */
+	isdir = create(buf, OREAD, DMDIR|0777);
+
+	/*
+	 *  make sure everyone can write here if it's a mailbox
+	 * rather than a folder
+	 */
+	if(mode == 0755)
+	if(isdir >= 0 && (d = dirfstat(isdir))){
+		d->mode |= 0773;
+		dirfwstat(isdir, d);
+		free(d);
+	}
+
+	if(isdir == -1){
+		fprint(2, "can't create %s: %s\n", ftype, buf);
+		return -1;
+	}
+	close(isdir);
+	return 0;
+}
+
+int
+createfolder(char *user, char *folder)
+{
+	return createfolder0(user, folder, "folder");
+}
+
+int
+creatembox(char *user, char *mbox)
+{
+	char buf[Pathlen];
+
+	if(mbox == 0)
+		snprint(buf, sizeof buf, "mbox");
+	else
+		snprint(buf, sizeof buf, "%s/mbox", mbox);
+	return createfolder0(user, buf, "mbox");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/common/mkfile
@@ -1,0 +1,18 @@
+</$objtype/mkfile
+
+LIB=libcommon.a$O
+OFILES=\
+	aux.$O\
+	become.$O\
+	config.$O\
+	folder.$O\
+	flags.$O\
+	fmt.$O\
+	libsys.$O\
+	process.$O\
+
+HFILES=\
+	common.h\
+	sys.h\
+
+</sys/src/cmd/mklib
--- /dev/null
+++ b/sys/src/cmd/upas/common/process.c
@@ -1,0 +1,172 @@
+#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)
+			become(av, who);
+		exec(av[0], av);
+		sysfatal("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] = 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[ERRMAX];
+
+	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(pp);
+	return 0;
+}
+
+/* kill a process */
+//extern int
+//proc_kill(process *pp)
+//{
+//	return syskill(pp->pid);
+//}
--- /dev/null
+++ b/sys/src/cmd/upas/common/sys.h
@@ -1,0 +1,63 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+/*
+ *  for the lock routines in libsys.c
+ */
+typedef struct Mlock	Mlock;
+struct Mlock {
+	int	fd;
+	int	pid;
+	char	name[Pathlen];
+};
+
+/*
+ *  from config.c
+ */
+extern char *MAILROOT;	/* root of mail system */
+extern char *SPOOL;	/* spool directory; for spam ctl */
+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 */
+
+enum {
+	Mboxmode	= 0622,
+};
+
+/*
+ *  files in libsys.c
+ */
+char	*sysname_read(void);
+char	*alt_sysname_read(void);
+char	*domainname_read(void);
+char	**sysnames_read(void);
+char	*getlog(void);
+Tmfmt	thedate(Tm*);
+Biobuf	*sysopen(char*, char*, ulong);
+int	sysopentty(void);
+int	sysclose(Biobuf*);
+int	sysmkdir(char*, ulong);
+Mlock	*syslock(char *);
+void	sysunlock(Mlock *);
+void	syslockrefresh(Mlock *);
+int	sysrename(char*, char*);
+int	sysexist(char*);
+int	syskill(int);
+int	syskillpg(int);
+Mlock	*trylock(char *);
+void	pipesig(int*);
+void	pipesigoff(void);
+int	holdon(void);
+void	holdoff(int);
+int	syscreatelocked(char*, int, int);
+int	sysopenlocked(char*, int);
+int	sysunlockfile(int);
+int	sysfiles(void);
+int 	become(char**, char*);
+int	sysdetach(void);
+char	*username(char*);
+int	creatembox(char*, char*);
+int	createfolder(char*, char*);
--- /dev/null
+++ b/sys/src/cmd/upas/dkim/dkim.c
@@ -1,0 +1,210 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <libsec.h>
+#include <auth.h>
+#include <authsrv.h>
+#include <pool.h>
+
+char *signhdr[] = {
+	"from:",
+	"to:",
+	"subject:",
+	"date:",
+	"message-id:",
+	nil
+};
+
+char *keyspec;
+char *domain;
+char *selector = "dkim";
+
+int
+trim(char *p)
+{
+	char *e;
+
+	for(e = p; *e != 0; e++)
+		if(*e == '\r' || *e == '\n')
+			break;
+	*e = 0;
+	return e - p;
+}		
+
+int
+usehdr(char *ln, char **hs)
+{
+	char **p;
+
+	for(p = signhdr; *p; p++)
+		if(cistrncmp(ln, *p, strlen(*p)) == 0){
+			if((*hs = realloc(*hs, strlen(*hs) + strlen(*p) + 1)) == nil)
+				sysfatal("realloc: %r");
+			strcat(*hs, *p);
+			return 1;
+		}
+	return 0;
+}
+
+void
+append(char **m, int *nm, int *sz, char *ln, int n)
+{
+	while(*nm + n + 2 >= *sz){
+		*sz += *sz/2;
+		*m = realloc(*m, *sz);
+	}
+	memcpy(*m + *nm, ln, n);
+	memcpy(*m + *nm + n, "\r\n", 2);
+	*nm += n+2;
+}
+
+
+int
+sign(uchar *hash, int nhash, char **sig, int *nsig)
+{
+	AuthRpc *rpc;
+	int afd;
+
+	if((afd = open("/mnt/factotum/rpc", ORDWR|OCEXEC)) < 0)
+		return -1;
+	if((rpc = auth_allocrpc(afd)) == nil){
+		close(afd);
+		return -1;
+	}
+	if(auth_rpc(rpc, "start", keyspec, strlen(keyspec)) != ARok){
+		auth_freerpc(rpc);
+		close(afd);
+		return -1;
+	}
+
+	if(auth_rpc(rpc, "write", hash, nhash) != ARok)
+		sysfatal("sign: write hash: %r");
+	if(auth_rpc(rpc, "read", nil, 0) != ARok)
+		sysfatal("sign: read sig: %r");
+	if((*sig = malloc(rpc->narg)) == nil)
+		sysfatal("malloc: %r");
+	*nsig = rpc->narg;
+	memcpy(*sig, rpc->arg, *nsig);
+	auth_freerpc(rpc);
+	close(afd);
+	return 0;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-s sel] -d dom\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, n, nhdr, nmsg, nsig, ntail, hdrsz, msgsz, use;
+	uchar hdrhash[SHA2_256dlen], msghash[SHA2_256dlen];
+	char *hdr, *msg, *sig, *ln, *hdrset, *dhdr;
+	Biobuf *rd, *wr;
+	DigestState *sh, *sb;
+
+	ARGBEGIN{
+	case 'd':
+		domain = EARGF(usage());
+		break;
+	case 's':
+		selector = EARGF(usage());
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	if(domain == nil)
+		usage();
+	fmtinstall('H', encodefmt);
+	fmtinstall('[', encodefmt);
+	keyspec = smprint("proto=rsa service=dkim role=sign hash=sha256 domain=%s", domain);
+
+	rd = Bfdopen(0, OREAD);
+	wr = Bfdopen(1, OWRITE);
+
+	nhdr = 0;
+	hdrsz = 32;
+	if((hdr = malloc(hdrsz)) == nil)
+		sysfatal("malloc: %r");
+	nmsg = 0;
+	msgsz = 32;
+	if((msg = malloc(msgsz)) == nil)
+		sysfatal("malloc: %r");
+
+	use = 0;
+	sh = nil;
+	hdrset = strdup("");
+	while((ln = Brdstr(rd, '\n', 1)) != nil){
+		n = trim(ln);
+		if(n == 0
+		|| (n == 1 && ln[0] == '\r' || ln[0] == '\n')
+		|| (n == 2 && strcmp(ln, "\r\n") == 0))
+			break;
+		/*
+		 * strip out existing DKIM signatures,
+		 * for the sake of mailing lists and such.
+		 */
+		if(cistrcmp(ln, "DKIM-Signature:") == 0)
+			continue;
+		if(ln[0] != ' ' && ln[0] != '\t')
+			use = usehdr(ln, &hdrset);
+		if(use){
+			sh = sha2_256((uchar*)ln, n, nil, sh);
+			sh = sha2_256((uchar*)"\r\n", 2, nil, sh);
+		}
+		append(&hdr, &nhdr, &hdrsz, ln, n);
+	}
+
+	sb = nil;
+	ntail = 0;
+	while((ln = Brdstr(rd, '\n', 0)) != nil){
+		n = trim(ln);
+		if(n == 0){
+			ntail++;
+			continue;
+		}
+		for(i = 0; i < ntail; i++){
+			sb = sha2_256((uchar*)"\r\n", 2, nil, sb);
+			append(&msg, &nmsg, &msgsz, "", 0);
+			ntail = 0;
+		}
+		sb = sha2_256((uchar*)ln, n, nil, sb);
+		sb = sha2_256((uchar*)"\r\n", 2, nil, sb);
+		append(&msg, &nmsg, &msgsz, ln, n);
+	}
+	if(nmsg == 0 || ntail > 1)
+		sb = sha2_256((uchar*)"\r\n", 2, nil, sb);
+	Bterm(rd);
+
+	sha2_256(nil, 0, msghash, sb);
+	dhdr = smprint(
+		"DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=%s;\r\n"
+		" h=%s; s=%s;\r\n"
+		" bh=%.*[; \r\n"
+		" b=",
+		domain, hdrset, selector,
+		(int)sizeof(msghash), msghash);
+	if(dhdr == nil)
+		sysfatal("smprint: %r");
+	sh = sha2_256((uchar*)dhdr, strlen(dhdr), nil, sh);
+	sha2_256(nil, 0, hdrhash, sh);
+	if(sign(hdrhash, sizeof(hdrhash), &sig, &nsig) == -1)
+		sysfatal("sign: %r");
+
+	Bwrite(wr, dhdr, strlen(dhdr));
+	Bprint(wr, "%.*[\r\n", nsig, sig);
+	Bwrite(wr, hdr, nhdr);
+	Bprint(wr, "\n");
+	Bwrite(wr, msg, nmsg);
+	Bterm(wr);
+
+	free(hdr);
+	free(msg);
+	free(sig);
+	exits(nil);	
+}
--- /dev/null
+++ b/sys/src/cmd/upas/dkim/mkfile
@@ -1,0 +1,7 @@
+</$objtype/mkfile
+
+TARG=dkim
+OFILES=dkim.$O
+
+</sys/src/cmd/mkone
+<../mkupas
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/dat.h
@@ -1,0 +1,8 @@
+typedef struct Addr Addr;
+struct Addr
+{
+	Addr *next;
+	char *val;
+};
+
+extern Addr* readaddrs(char*, Addr*);
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/deliver.c
@@ -1,0 +1,40 @@
+/*
+ * deliver recipient fromfile mbox - append stdin to mbox with locking & logging
+ */
+#include "dat.h"
+#include "common.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: deliver recipient fromaddr-file mbox\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *to;
+	Tmfmt tf;
+	Tm tm;
+	int r;
+	Addr *a;
+
+	ARGBEGIN{
+	}ARGEND;
+	tmfmtinstall();
+	if(argc != 3)
+		usage();
+	if(to = strrchr(argv[0], '!'))
+		to++;
+	else
+		to = argv[0];
+	a = readaddrs(argv[1], nil);
+	if(a == nil)
+		sysfatal("missing from address");
+	tf = thedate(&tm);
+	werrstr("");
+	r = fappendfolder(a->val, tmnorm(&tm), argv[2], 0);
+	syslog(0, "mail", "delivered %s From %s %τ (%s) %d %r", to, a->val, tf, argv[0], r);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/list.c
@@ -1,0 +1,322 @@
+#include <u.h>
+#include <libc.h>
+#include <regexp.h>
+#include <libsec.h>
+#include <bio.h>
+#include <String.h>
+#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 add|check patternfile [addressfile ...]\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, dotlim;
+	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, and including, 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,
+	 * except for .uk, which needs three.
+	 */
+	s_append(s, "(.*\\.)?");
+	p = addr+strlen(addr);			/* point at NUL */
+	if (p[-1] == '.')
+		*--p = '\0';
+	if (p - addr > 3 && strcmp(".uk", p - 3) == 0)
+		dotlim = 3;
+	else
+		dotlim = 2;
+	dots = 0;
+	while(--p > at)
+		if(*p == '.' && ++dots >= dotlim){
+			p++;
+			break;
+		}
+	for(; *p; p++){
+		if(strchr(".*+?(|)\\[]^$", *p) != nil)
+			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();
+	tmfmtinstall();
+
+	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();
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/mbappend.c
@@ -1,0 +1,64 @@
+/*
+ * deliver to one's own folder with locking & logging
+ */
+#include "dat.h"
+#include "common.h"
+
+void
+append(int fd, char *mb, char *from, long t)
+{
+	char *folder;
+	Tm tm;
+	int r;
+
+	folder = foldername(from, getuser(), mb);
+	r = fappendfolder(0, t, folder, fd);
+	if(r == 0)
+		werrstr("");
+	syslog(0, "mail", "mbappend %s %τ (%s) %r", mb, thedate(&tm), folder);
+	if(r)
+		exits("fail");
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: mbappend [-t time] [-f from] mbox [file ...]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *mb, *from;
+	int fd;
+	long t;
+
+	from = nil;
+	t = time(0);
+	tmfmtinstall();
+	ARGBEGIN{
+	case 't':
+		t = strtoul(EARGF(usage()), 0, 0);
+		break;
+	case 'f':
+		from = EARGF(usage());
+		break;
+	default:
+		usage();
+	}ARGEND;
+	if(*argv == 0)
+		usage();
+	werrstr("");
+	mb = *argv++;
+	if(*argv == 0)
+		append(0, mb, from, t);
+	else for(; *argv; argv++){
+		fd = open(*argv, OREAD);
+		if(fd < 0)
+			sysfatal("open: %r");
+		append(fd, mb, from, t);
+		close(fd);
+	}
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/mbcreate.c
@@ -1,0 +1,33 @@
+#include "dat.h"
+#include "common.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: mbcreate [-f] ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int r;
+	int (*f)(char*, char*);
+
+	f = creatembox;
+	ARGBEGIN{
+	case 'f':
+		f = createfolder;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	r = 0;
+	tmfmtinstall();
+	for(; *argv; argv++)
+		r |= f(getuser(), *argv);
+	if(r)
+		exits("errors");
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/mbremove.c
@@ -1,0 +1,244 @@
+/*
+ * why did i write this and not use upas/fs?
+ */
+#include "dat.h"
+#include "common.h"
+
+int	qflag;
+int	pflag;
+int	rflag;
+int	tflag;
+int	vflag;
+
+/* must be [0-9]+(\..*)? */
+static int
+dirskip(Dir *a, uvlong *uv)
+{
+	char *p;
+
+	if(a->length == 0)
+		return 1;
+	*uv = strtoul(a->name, &p, 0);
+	if(*uv < 1000000 || *p != '.')
+		return 1;
+	*uv = *uv<<8 | strtoul(p+1, &p, 10);
+	if(*p)
+		return 1;
+	return 0;
+}
+
+static int
+ismbox(char *path)
+{
+	char buf[512];
+	int fd, r;
+
+	fd = open(path, OREAD);
+	if(fd == -1)
+		return 0;
+	r = 1;
+	if(read(fd, buf, sizeof buf) < 28+5)
+		r = 0;
+	else if(strncmp(buf, "From ", 5))
+		r = 0;
+	close(fd);
+	return r;
+}
+
+int
+isindex(Dir *d)
+{
+	char *p;
+
+	p = strrchr(d->name, '.');
+	if(!p)
+		return -1;
+	if(strcmp(p, ".idx") || strcmp(p, ".imp"))
+		return 1;
+	return 0;
+}
+
+int
+idiotcheck(char *path, Dir *d, int getindex)
+{
+	uvlong v;
+
+	if(d->mode & DMDIR)
+		return 0;
+	if(!strncmp(d->name, "L.", 2))
+		return 0;
+	if(getindex && isindex(d))
+		return 0;
+	if(!dirskip(d, &v) || ismbox(path))
+		return 0;
+	return -1;
+}
+
+int
+vremove(char *buf)
+{
+	if(vflag)
+		fprint(2, "rm %s\n", buf);
+	if(!pflag)
+		return remove(buf);
+	return 0;
+}
+
+int
+rm(char *dir, int level)
+{
+	char buf[Pathlen];
+	int i, n, r, fd, isdir;
+	Dir *d;
+
+	d = dirstat(dir);
+	isdir = d->mode & DMDIR;
+	free(d);
+	if(!isdir)
+		return 0;
+	fd = open(dir, OREAD);
+	if(fd == -1)
+		return -1;
+	n = dirreadall(fd, &d);
+	close(fd);
+	r = 0;
+	for(i = 0; i < n; i++){
+		snprint(buf, sizeof buf, "%s/%s", dir, d[i].name);
+		if(rflag)
+			r |= rm(buf, level+1);
+		if(idiotcheck(buf, d+i, level+rflag) == -1)
+			continue;
+		if(vremove(buf) != 0)
+			r = -1;
+	}
+	free(d);
+	return r;
+}
+
+void
+nukeidx(char *buf)
+{
+	char buf2[Pathlen];
+
+	snprint(buf2, sizeof buf2, "%s.idx", buf);
+	vremove(buf2);
+	snprint(buf2, sizeof buf2, "%s.imp", buf);
+	vremove(buf2);
+}
+
+void
+truncidx(char *buf)
+{
+	char buf2[Pathlen];
+
+	snprint(buf2, sizeof buf2, "%s.idx", buf);
+	vremove(buf2);
+//	snprint(buf2, sizeof buf2, "%s.imp", buf);
+//	vremove(buf2);
+}
+
+static int
+removefolder0(char *user, char *folder, char *ftype)
+{
+	char *msg, buf[Pathlen];
+	int r, isdir;
+	Dir *d;
+
+	assert(folder != 0);
+	mboxpathbuf(buf, sizeof buf, user, folder);
+	if((d = dirstat(buf)) == 0){
+		fprint(2, "%s: %s doesn't exist\n", buf, ftype);
+		return 0;
+	}
+	isdir = d->mode & DMDIR;
+	free(d);
+	msg = "deleting";
+	if(tflag)
+		msg = "truncating";
+	fprint(2, "%s %s: %s\n", msg, ftype, buf);
+
+	/* must match folder.c:/^openfolder */
+	r = rm(buf, 0);
+	if(!tflag)
+		r = vremove(buf);
+	else if(!isdir)
+		r = open(buf, OWRITE|OTRUNC);
+
+	if(tflag)
+		truncidx(buf);
+	else
+		nukeidx(buf);
+
+	if(r == -1){
+		fprint(2, "%s: can't %s %s\n", buf, msg, ftype);
+		return -1;
+	}
+	close(r);
+	return 0;
+}
+
+int
+removefolder(char *user, char *folder)
+{
+	return removefolder0(user, folder, "folder");
+}
+
+int
+removembox(char *user, char *mbox)
+{
+	char buf[Pathlen];
+
+	if(mbox == 0)
+		snprint(buf, sizeof buf, "mbox");
+	else
+		snprint(buf, sizeof buf, "%s/mbox", mbox);
+	return removefolder0(user, buf, "mbox");
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: mbremove [-fpqrtv] ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int r;
+	int (*f)(char*, char*);
+
+	f = removembox;
+	ARGBEGIN{
+	case 'f':
+		f = removefolder;
+		break;
+	case 'p':
+		pflag++;
+		break;
+	case 'q':
+		qflag++;
+		close(2);
+		open("/dev/null", OWRITE);
+		break;
+	case 'r':
+		rflag++;
+		break;
+	case 't':
+		tflag++;
+		break;
+	case 'v':
+		vflag++;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	r = 0;
+	tmfmtinstall();
+	for(; *argv; argv++)
+		r |= f(getuser(), *argv);
+	if(r)
+		exits("errors");
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/mkfile
@@ -1,0 +1,14 @@
+</$objtype/mkfile
+
+TARG=\
+	deliver\
+	list\
+	mbappend\
+	token\
+
+LIB=../common/libcommon.a$O
+OFILES=readaddrs.$O
+
+</sys/src/cmd/mkmany
+<../mkupas
+CFLAGS=$CFLAGS -I../common
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/pipefrom.sample
@@ -1,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
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/pipeto.sample
@@ -1,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}}<<EOF
+Subject: $USER's mail filter
+I've been getting so much junk mail that I'm resorting to
+a draconian mechanism to avoid the mail.  In order
+to make sure that there's a real person sending mail, I'm
+asking you to explicitly enable access.  To do that, send
+mail to $USER at this domain with the token:
+	$TOKEN
+in the subject of your mail message.  After that, you
+shouldn't get any bounces from me.  Sorry if this is
+an inconvenience.
+
+----------------
+Original message
+----------------
+EOF
+
+echo `{date} bounced $RECIP From `{cat $D/replyto} \
+	>> /mail/box/$USER/_bounced >[2] /dev/null
+
+rv=$status
+rm $TMP
+exit $status
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/pipeto.sample-hold
@@ -1,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
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/readaddrs.c
@@ -1,0 +1,97 @@
+#include <u.h>
+#include <libc.h>
+#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;
+		}
+}
+
+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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/testemail
@@ -1,0 +1,4 @@
+From: erik quanstrom <[email protected]>
+Subject: 1 testing
+
+testing
--- /dev/null
+++ b/sys/src/cmd/upas/filterkit/token.c
@@ -1,0 +1,75 @@
+#include <u.h>
+#include <libc.h>
+#include <libsec.h>
+#include "dat.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: token key [token]\n");
+	exits("usage");
+}
+
+static char*
+mktoken(char *key, long t)
+{
+	char *now, token[64];
+	uchar digest[SHA1dlen];
+
+	now = ctime(t);
+	memset(now+11, ':', 8);
+	hmac_sha1((uchar*)now, strlen(now), (uchar*)key, strlen(key), digest, nil);
+	enc64(token, sizeof token, digest, sizeof digest);
+	return smprint("%.5s", token);
+}
+
+static char*
+check_token(char *key, char *file)
+{
+	char *s, buf[1024];
+	int i, fd, m;
+	long now;
+
+	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);
+		m = s != nil && strstr(buf, s) != nil;
+		free(s);
+		if(m)
+			return nil;
+	}
+	return "no match";
+}
+
+static char*
+create_token(char *key)
+{
+	print("%s", mktoken(key, time(0)));
+	return nil;
+}
+
+void
+main(int argc, char **argv)
+{
+	ARGBEGIN {
+	} ARGEND;
+
+	tmfmtinstall();
+	switch(argc){
+	case 2:
+		exits(check_token(argv[0], argv[1]));
+	case 1:
+		exits(create_token(argv[0]));
+	default:
+		usage();
+	}
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/bos.c
@@ -1,0 +1,40 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+/*
+ * assume:
+ * - the stack segment can't be resized
+ * - stacks may not be segattached (by name Stack, anyway)
+ * - no thread library
+ */
+uintptr
+absbos(void)
+{
+	char buf[64], *f[10], *s, *r;
+	int n;
+	uintptr p, q;
+	Biobuf *b;
+
+	p = 0xd0000000;	/* guess pc kernel */
+	snprint(buf, sizeof buf, "/proc/%ud/segment", getpid());
+	b = Bopen(buf, OREAD);
+	if(b == nil)
+		return p;
+	for(; s = Brdstr(b, '\n', 1); free(s)){
+		if((n = tokenize(s, f, nelem(f))) < 3)
+			continue;
+		if(strcmp(f[0], "Stack") != 0)
+			continue;
+		/*
+		 * addressing from end because segment
+		 * flags could become discontiguous  if
+		 * additional flags are added
+		 */
+		q = strtoull(f[n - 3], &r, 16);
+		if(*r == 0 && (char*)q > end)
+			p = q;
+	}
+	Bterm(b);
+	return p;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/cache.c
@@ -1,0 +1,416 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+static void
+addlru(Mailbox *c, Message *m)
+{
+	Message *l, **ll;
+
+	if((m->cstate & (Cheader|Cbody)) == 0)
+		return;
+
+	assert(c->fetch != nil);
+
+	c->nlru++;
+	ll = &c->lru;
+	while((l = *ll) != nil){
+		if(l == m){
+			c->nlru--;
+			*ll = m->lru;
+		} else {
+			ll = &l->lru;
+		}
+	}
+	m->lru = nil;
+	*ll = m;
+}
+
+static void
+notecache(Mailbox *mb, Message *m, long sz)
+{
+	assert(Topmsg(mb, m));
+	assert(sz >= 0 && sz <= Maxmsg);
+	m->csize += sz;
+	mb->cached += sz;
+	addlru(mb, m);
+}
+
+void
+cachefree(Mailbox *mb, Message *m)
+{
+	long i;
+	Message *s, **ll;
+
+	if(Topmsg(mb, m) && mb->fetch != nil){
+		for(ll = &mb->lru; *ll != nil; ll = &((*ll)->lru)){
+			if(*ll == m){
+				mb->nlru--;
+				*ll = m->lru;
+				m->lru = nil;
+				break;
+			}
+		}
+		if(mb->decache != nil)
+			mb->decache(mb, m);
+		mb->cached -= m->csize;
+	}
+	for(s = m->part; s; s = s->next)
+		cachefree(mb, s);
+
+	if(m->mallocd){
+		free(m->start);
+		m->mallocd = 0;
+	}
+	if(m->ballocd){
+		free(m->body);
+		m->ballocd = 0;
+	}
+	if(m->hallocd){
+		free(m->header);
+		m->hallocd = 0;
+	}
+
+	for(i = 0; i < nelem(m->references); i++){
+		free(m->references[i]);
+		m->references[i] = nil;
+	}
+	free(m->unixfrom);
+	m->unixfrom = nil;
+	m->unixdate = nil;
+	free(m->unixheader);
+	m->unixheader = nil;
+	free(m->boundary);
+	m->boundary = nil;
+
+	m->csize = 0;
+	m->start = nil;
+	m->end = nil;
+	m->header = nil;
+	m->hend = nil;
+	m->hlen = -1;
+	m->body = nil;
+	m->bend = nil;
+	m->mheader = nil;
+	m->mhend = nil;
+	m->decoded = 0;
+	m->converted = 0;
+	m->badchars = 0;
+	m->cstate &= ~(Cheader|Cbody);
+}
+
+void
+putcache(Mailbox *mb, Message *m)
+{
+	int n;
+
+	if(mb->fetch == nil)
+		return;
+	while(!Topmsg(mb, m)) m = m->whole;
+	addlru(mb, m);
+	while(mb->lru != nil && (mb->cached > cachetarg || mb->nlru > 10)){
+		n = 0;
+		while(mb->lru->refs > 0){
+			if(++n >= mb->nlru)
+				return;
+			addlru(mb, mb->lru);
+		}
+		cachefree(mb, mb->lru);
+	}
+}
+
+static int
+squeeze(Message *m, uvlong o, long l, int c)
+{
+	char *p, *q, *e;
+	int n;
+
+	q = memchr(m->start + o, c, l);
+	if(q == nil)
+		return 0;
+	n = 0;
+	e = m->start + o + l;
+	for(p = q; q < e; q++){
+		if(*q == c){
+			n++;
+			continue;
+		}
+		*p++ = *q;
+	}
+	return n;
+}
+
+void
+msgrealloc(Message *m, ulong l)
+{
+	long l0, h0, m0, me, b0;
+
+	l0 = m->end - m->start;
+	m->mallocd = 1;
+	h0 = m->hend - m->start;
+	m0 = m->mheader - m->start;
+	me = m->mhend - m->start;
+	b0 = m->body - m->start;
+	assert(h0 >= 0 && m0 >= 0 && me >= 0 && b0 >= 0);
+	m->start = erealloc(m->start, l + 1);
+	m->rbody = m->start + b0;
+	m->rbend = m->end = m->start + l0;
+	if(!m->hallocd){
+		m->header = m->start;
+		m->hend = m->start + h0;
+	}
+	if(!m->ballocd){
+		m->body = m->start + b0;
+		m->bend = m->start + l0;
+	}
+	m->mheader = m->start + m0;
+	m->mhend = m->start + me;
+}
+
+/*
+ * the way we squeeze out bad characters is exceptionally sneaky.
+ */
+static int
+fetch(Mailbox *mb, Message *m, uvlong o, ulong l)
+{
+	int expand;
+	long l0, n, sz0;
+
+top:
+	l0 = m->end - m->start;
+	assert(l0 >= 0);
+	dprint("fetch %lud sz %lud o %llud l %lud badchars %d\n", l0, m->size, o, l, m->badchars);
+	if(l0 == m->size || o > m->size)
+		return 0;
+	expand = 0;
+	if(o + l > m->size)
+		l = m->size - o;
+	if(o + l == m->size)
+		l += m->ibadchars - m->badchars;
+	if(o + l > l0){
+		expand = 1;
+		msgrealloc(m, o + m->badchars + l);
+	}
+	assert(l0 <= o);
+	sz0 = m->size;
+	if(mb->fetch(mb, m, o + m->badchars, l) == -1){
+		logmsg(m, "can't fetch %D %llud %lud", m->fileid, o, l);
+		m->deleted = Dead;
+		return -1;
+	}
+	if(m->size - sz0)
+		l += m->size - sz0;	/* awful botch for gmail */
+	if(expand){
+		/* grumble.  poor planning. */
+		if(m->badchars > 0)
+			memmove(m->start + o, m->start + o + m->badchars, l);
+		n = squeeze(m, o, l, 0);
+		n += squeeze(m, o, l - n, '\r');
+		if(n > 0){
+			if(m->ibadchars == 0)
+				dprint("   %ld more badchars\n", n);
+			l -= n;
+			m->badchars += n;
+			msgrealloc(m, o + l);
+		}
+		notecache(mb, m, l);
+		m->bend = m->rbend = m->end = m->start + o + l;
+		if(n)
+		if(o + l + n == m->size && m->cstate&Cidx){
+			dprint("   redux %llud %ld\n", o + l, n);
+			o += l;
+			l = n;
+			goto top;
+		}
+	}else
+		eprint("unhandled case in fetch\n");
+	*m->end = 0;
+	return 0;
+}
+
+void
+cachehash(Mailbox *mb, Message *m)
+{
+	assert(mb->refs >= 0);
+	if(mb->refs == 0)
+		return;
+	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);
+	henter(PATH(m->id, Qdir), "xxx",
+		(Qid){PATH(m->id, Qmax), 0, QTFILE}, m, mb);	/* sleezy speedup */
+}
+
+static char *itab[] = {
+	"idx",
+	"stale",
+	"header",
+	"body",
+	"new",
+};
+
+char*
+cstate(Message *m)
+{
+	char *p, *e;
+	int i, s;
+	static char buf[64];
+
+	s = m->cstate;
+	p = e = buf;
+	e += sizeof buf;
+	for(i = 0; i < 8; i++)
+		if(s & 1<<i)
+		if(i < nelem(itab))
+			p = seprint(p, e, "%s ", itab[i]);
+	if(p > buf)
+		p--;
+	p[0] = 0;
+	return buf;
+}
+
+
+static int
+middlecache(Mailbox *mb, Message *m)
+{
+	int y;
+
+	y = 0;
+	while(!Topmsg(mb, m)){
+		m = m->whole;
+		if((m->cstate & Cbody) == 0)
+			y = 1;
+	}
+	if(y == 0)
+		return 0;
+	dprint("middlecache %lud [%D] %lud %lud\n",
+		m->id, m->fileid, (ulong)(m->end - m->start), m->size);
+	return cachebody(mb, m);
+}
+
+int
+cacheheaders(Mailbox *mb, Message *m)
+{
+	char *p, *e;
+	int r;
+	ulong o;
+
+	if(!mb->fetch || m->cstate&Cheader)
+		return 0;
+	if(!Topmsg(mb, m))
+		return middlecache(mb, m);
+	dprint("cacheheaders %lud %D\n", m->id, m->fileid);
+	if(m->size < 10000)
+		r = fetch(mb, m, 0, m->size);
+	else for(r = 0; (o = m->end - m->start) < m->size; ){
+		if((r = fetch(mb, m, o, 4096)) < 0)
+			break;
+		p = m->start + o;
+		if(o)
+			p--;
+		for(e = m->end - 2; p < e; p++){
+			p = memchr(p, '\n', e - p);
+			if(p == nil)
+				break;
+			if(p[1] == '\n' || (p[1] == '\r' && p[2] == '\n'))
+				goto found;
+		}
+	}
+	if(r < 0)
+		return -1;
+found:
+	parseheaders(mb, m, mb->addfrom, 0);
+	return 0;
+}
+
+void
+digestmessage(Mailbox *mb, Message *m)
+{
+	Message *old;
+
+	assert(m->digest == nil);
+	m->digest = emalloc(SHA1dlen);
+	sha1((uchar*)m->start, m->end - m->start, m->digest, nil);
+	old = mtreeadd(mb, m);
+	if(old != nil && old != m){
+		m = mtreeadd(mb, old);
+		logmsg(m, "dup detected");
+		m->deleted = Dup;	/* no dups allowed */
+	}
+	dprint("%lud %#A\n", m->id, m->digest);
+}
+
+int
+cachebody(Mailbox *mb, Message *m)
+{
+	ulong o;
+
+	while(!Topmsg(mb, m))
+		m = m->whole;
+	if(mb->fetch == nil || m->cstate&Cbody)
+		return 0;
+	o = m->end - m->start;
+	dprint("cachebody %lud [%D] %lud %lud %s\n", m->id, m->fileid, o, m->size, cstate(m));
+	if(o < m->size)
+	if(fetch(mb, m, o, m->size - o) < 0)
+		return -1;
+	if((m->cstate&Cidx) == 0){
+		assert(m->ibadchars == 0);
+		if(m->badchars > 0)
+			dprint("reducing size %ld %ld\n", m->size, m->size - m->badchars);
+		m->size -= m->badchars;		/* sneaky */
+		m->ibadchars = m->badchars;
+	}
+	if(m->digest == nil)
+		digestmessage(mb, m);
+	if(m->lines == 0)
+		m->lines = countlines(m);
+	parse(mb, m, mb->addfrom, 0);
+	dprint("  →%s\n", cstate(m));
+	return 0;
+}
+
+int
+cacheidx(Mailbox *mb, Message *m)
+{
+	if(m->cstate & Cidx)
+		return 0;
+	if(cachebody(mb, m) < 0)
+		return -1;
+	m->cstate |= Cidxstale|Cidx;
+	return 0;
+}
+
+static int
+countparts(Message *m)
+{
+	Message *p;
+
+	if(m->nparts == 0)
+		for(p = m->part; p; p = p->next){
+			countparts(p);
+			m->nparts++;
+		}
+	return m->nparts;
+}
+
+int
+ensurecache(Mailbox *mb, Message *m)
+{
+	if((m->deleted & ~Deleted) != 0 || !m->inmbox)
+		return -1;
+	msgincref(mb, m);
+	cacheidx(mb, m);
+	if((m->cstate & Cidx) == 0){
+		logmsg(m, "%s: can't cache: %s: %r", mb->path, m->name);
+		msgdecref(mb, m);
+		return -1;
+	}
+	if(m->digest == nil)
+		sysfatal("digest?");
+	countparts(m);
+	return 0;
+}
binary files /dev/null b/sys/src/cmd/upas/fs/chkidx differ
--- /dev/null
+++ b/sys/src/cmd/upas/fs/chkidx.c
@@ -1,0 +1,416 @@
+#include "common.h"
+#include <auth.h>
+#include <libsec.h>
+#include <bin.h>
+#include "dat.h"
+
+#define idprint(...)	if(1) fprint(2, __VA_ARGS__); else {}
+enum{
+	Maxver	= 10,
+};
+static char *magictab[Maxver] = {
+[4]	"idx magic v4\n",
+[7]	"idx magic v7\n",
+};
+static int fieldstab[Maxver] = {
+[4]	19,
+[7]	21,
+};
+
+static	char	*magic;
+static	int	Idxfields;
+static	int	lineno;
+static	int	idxver;
+
+int
+newid(void)
+{
+	static int id;
+
+	return ++id;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+
+	p = mallocz(n, 1);
+	if(!p)
+		sysfatal("malloc %lud: %r", n);
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+	
+static int
+Afmt(Fmt *f)
+{
+	char buf[SHA1dlen*2 + 1];
+	uchar *u, i;
+
+	u = va_arg(f->args, uchar*);
+	if(u == 0 && f->flags & FmtSharp)
+		return fmtstrcpy(f, "-");
+	if(u == 0)
+		return fmtstrcpy(f, "<nildigest>");
+	for(i = 0; i < SHA1dlen; i++)
+		sprint(buf + 2*i, "%2.2ux", u[i]);
+	return fmtstrcpy(f, buf);
+}
+
+static int
+Dfmt(Fmt *f)
+{
+	char buf[32];
+	int seq;
+	uvlong v;
+
+	v = va_arg(f->args, uvlong);
+	seq = v & 0xff;
+	if(seq > 99)
+		seq = 99;
+	snprint(buf, sizeof buf, "%llud.%.2d", v>>8, seq);
+	return fmtstrcpy(f, buf);
+}
+
+static Mailbox*
+shellmailbox(char *path)
+{
+	Mailbox *mb;
+
+	mb = malloc(sizeof *mb);
+	if(mb == 0)
+		sysfatal("malloc");
+	memset(mb, 0, sizeof *mb);
+	snprint(mb->path, sizeof mb->path, "%s", path);
+	mb->id = newid();
+	mb->root = newmessage(nil);
+	mb->mtree = mkavltree(mtreecmp);
+	return mb;
+}
+
+void
+shellmailboxfree(Mailbox*)
+{
+}
+
+Message*
+newmessage(Message *parent)
+{
+	static int id;
+	Message *m;
+
+//	msgallocd++;
+
+	m = mallocz(sizeof *m, 1);
+	if(m == 0)
+		sysfatal("malloc");
+	m->disposition = Dnone;
+//	m->type = newrefs("text/plain");
+//	m->charset = newrefs("iso-8859-1");
+	m->cstate = Cidxstale;
+	m->flags = Frecent;
+	m->id = newid();
+	if(parent)
+		snprint(m->name, sizeof m->name, "%d", ++(parent->subname));
+	if(parent == nil)
+		parent = m;
+	m->whole = parent;
+	m->hlen = -1;
+	return m;
+}
+
+void
+unnewmessage(Mailbox *mb, Message *parent, Message *m)
+{
+	assert(parent->subname > 0);
+//	delmessage(mb, m);
+	USED(mb, m);
+	parent->subname -= 1;
+}
+
+
+static int
+validmessage(Mailbox *mb, Message *m, int level)
+{
+	if(level){
+		if(m->digest != 0)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+		if(m->fileid != 0)
+			goto lose;
+	}else{
+		if(m->digest == 0)
+			goto lose;
+		if(m->size == 0)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+			goto lose;
+		if(mtreefind(mb, m->digest))
+			goto lose;
+	}
+	return 1;
+lose:
+	fprint(2, "invalid cache[%d] %#A size %ld %D\n", level, m->digest, m->size, m->fileid);
+	return 0;
+}
+
+static char*
+∫(char *x)
+{
+	if(x && *x)
+		return x;
+	return nil;
+}
+
+static char*
+brdstr(Biobuf *b, int c, int eat)
+{
+	char *s;
+
+	s = Brdstr(b, c, eat);
+	if(s)
+		lineno++;
+	return s;
+}
+
+static int
+nibble(int c)
+{
+	if(c >= '0' && c <= '9')
+		return c - '0';
+	if(c < 0x20)
+		c += 0x20;
+	if(c >= 'a' && c <= 'f')
+		return c - 'a'+10;
+	return 0xff;
+}
+
+static uchar*
+hackdigest(char *s)
+{
+	uchar t[SHA1dlen];
+	int i;
+
+	if(strcmp(s, "-") == 0)
+		return 0;
+	if(strlen(s) != 2*SHA1dlen){
+		fprint(2, "bad digest %s\n", s);
+		return 0;
+	}
+	for(i = 0; i < SHA1dlen; i++)
+		t[i] = nibble(s[2*i])<<4 | nibble(s[2*i + 1]);
+	memmove(s, t, SHA1dlen);
+	return (uchar*)s;
+}
+
+static Message*
+findmessage(Mailbox *, Message *parent, int n)
+{
+	Message *m;
+
+	for(m = parent->part; m; m = m->next)
+		if(!m->digest && n-- == 0)
+			return m;
+	return 0;
+}
+
+static uvlong
+rdfileid(char *s, int level)
+{
+	char *p;
+	uvlong uv;
+
+	uv = strtoul(s, &p, 0);
+	if((level == 0 && uv < 1000000) || *p != '.')
+		return 0;
+	return uv<<8 | strtoul(p + 1, 0, 10);
+}
+
+static int
+rdidx(Biobuf *b, Mailbox *mb, Message *parent, int npart, int level)
+{
+	char *f[50 + 1], *s;
+	uchar *digest;
+	int n, nparts, good, bad, redux;
+	Message *m, **ll, *l;
+
+	bad = good = redux = 0;
+	ll = &parent->part;
+	nparts = npart;
+	for(; npart != 0 && (s = brdstr(b, '\n', 1)); npart--){
+//if(lineno>18&&lineno<25)idprint("%d: %d [%s]\n", lineno, level, s);
+		n = tokenize(s, f, nelem(f));
+		if(n != Idxfields){
+			print("%d: bad line\n", lineno);
+			bad++;
+			free(s);
+			continue;
+		}
+		digest = hackdigest(f[0]);
+		if(level == 0){
+			if(digest == 0)
+				idprint("%d: no digest\n", lineno);
+			m = mtreefind(mb, digest);
+		}else{
+			m = findmessage(mb, parent, nparts - npart);
+			if(m == 0){
+			//	idprint("can't find message\n");
+			}
+		}
+		if(m){
+			/*
+			 * read in mutable information.
+			 * currently this is only flags
+			 */
+			idprint("%d seen before %d... %.2ux", level, m->id, m->cstate);
+			redux++;
+			m->flags |= strtoul(f[1], 0, 16);
+			m->cstate &= ~Cidxstale;
+			m->cstate |= Cidx;
+			idprint("→%.2ux\n", m->cstate);
+			free(s);
+
+			if(m->nparts)
+				rdidx(b, mb, m, m->nparts, level + 1);
+			ll = &m->next;
+			continue;
+		}
+		m = newmessage(parent);
+//if(lineno>18&&lineno<25)idprint("%d: %d %d %A\n", lineno, level, m->id, digest);
+//		idprint("%d new %d %#A \n", level, m->id, digest);
+		m->digest = digest;
+		m->flags = strtoul(f[1], 0, 16);
+		m->fileid = rdfileid(f[2], level);
+		m->lines = atoi(f[3]);
+		m->ffrom = ∫(f[4]);
+		m->from = ∫(f[5]);
+		m->to = ∫(f[6]);
+		m->cc = ∫(f[7]);
+		m->bcc = ∫(f[8]);
+		m->replyto = ∫(f[9]);
+		m->messageid = ∫(f[10]);
+		m->subject = ∫(f[11]);
+		m->sender = ∫(f[12]);
+		m->inreplyto = ∫(f[13]);
+//		m->type = newrefs(f[14]);
+		m->disposition = atoi(f[15]);
+		m->size = strtoul(f[16], 0, 0);
+		m->rawbsize = strtoul(f[17], 0, 0);
+		switch(idxver){
+		case 4:
+			m->nparts = strtoul(f[18], 0, 0);
+		case 7:
+			m->ibadchars = strtoul(f[18], 0, 0);
+			m->idxaux = ∫(f[19]);
+			m->nparts = strtoul(f[20], 0, 0);
+		}
+		m->cstate &= ~Cidxstale;
+		m->cstate |= Cidx;
+		m->str = s;
+//		free(s);
+		if(!validmessage(mb, m, level)){
+			/*
+			 *  if this was an okay message, and somebody
+			 * wrote garbage to the index file, we lose the msg.
+			 */
+			print("%d: !validmessage\n", lineno);
+			bad++;
+			unnewmessage(mb, parent, m);
+			continue;
+		}
+		if(level == 0)
+			m->inmbox = 1;
+//		cachehash(mb, m);		/* hokey */
+		l = *ll;
+		*ll = m;
+		ll = &m->next;
+		*ll = l;
+		good++;
+
+		if(m->nparts){
+//			fprint(2, "%d: %d parts [%s]\n", lineno, m->nparts, f[18]);
+			rdidx(b, mb, m, m->nparts, level + 1);
+		}
+	}
+	if(level == 0)
+		print("idx: %d %d %d\n", good, bad, redux);
+	return 0;
+}
+
+static int
+verscmp(Biobuf *b)
+{
+	char *s;
+	int i;
+
+	if((s = brdstr(b, '\n', 0)) == 0)
+		return -1;
+	for(i = 0; i < Maxver; i++)
+		if(magictab[i])
+		if(strcmp(s, magictab[i]) == 0)
+			break;
+	free(s);
+	if(i == Maxver)
+		return -1;
+	idxver = i;
+	magic = magictab[i];
+	Idxfields = fieldstab[i];
+	fprint(2, "version %d\n", i);
+	return 0;
+}
+
+int
+mbvers(Biobuf *b)
+{
+	char *s;
+
+	if(s = brdstr(b, '\n', 1)){
+		free(s);
+		return 0;
+	}
+	return -1;
+}
+
+int
+ckidxfile(Mailbox *mb)
+{
+	char buf[Pathlen + 4];
+	int r;
+	Biobuf *b;
+
+	snprint(buf, sizeof buf, "%s", mb->path);
+	b = Bopen(buf, OREAD);
+	if(b == nil)
+		return -1;
+	if(verscmp(b) == -1)
+		return -1;
+	if(idxver >= 7)
+		mbvers(b);
+	r = rdidx(b, mb, mb->root, -1, 0);
+	Bterm(b);
+	return r;
+}
+
+static char *bargv[] = {"/fd/0", 0};
+
+void
+main(int argc, char **argv)
+{
+	Mailbox *mb;
+
+	fmtinstall('A', Afmt);
+	fmtinstall('D', Dfmt);
+	ARGBEGIN{
+	}ARGEND
+	if(*argv == 0)
+		argv = bargv;
+	for(; *argv; argv++){
+		mb = shellmailbox(*argv);
+		lineno = 0;
+		if(ckidxfile(mb) == -1)
+			fprint(2, "%s: %r\n", *argv);
+		shellmailboxfree(mb);
+	}
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/dat.h
@@ -1,0 +1,360 @@
+#include <avl.h>
+
+enum {
+	/* cache states */
+	Cidx		= 1<<0,
+	Cidxstale	= 1<<1,
+	Cheader 	= 1<<2,
+	Cbody		= 1<<3,
+	Cnew		= 1<<4,
+	Cmod		= 1<<5,
+
+	/* encodings */
+	Enone=	0,
+	Ebase64,
+	Equoted,
+
+	/* dispositions */
+	Dnone=	0,
+	Dinline,
+	Dfile,
+	Dignore,
+
+	/* mb create flags */
+	DMcreate	=  0x02000000,
+
+	/* rm flags */
+	Rrecur		= 1<<0,
+	Rtrunc		= 1<<1,
+
+	/* m->deleted flags */
+	Deleted		= 1<<0,
+	Dup		= 1<<1,
+	Dead		= 1<<2,
+	Disappear	= 1<<3,
+	Dmark		= 1<<4,	/* temporary mark for idx scan */
+
+	Maxmsg		= 75*1024*1024,	/* maxmessage size; debugging */
+	Maxcache	= 512*1024,	/* target cache size; set low for debugging */
+	Nctab		= 15,		/* max # of cached messages >10 */
+	Nref		= 10,
+};
+
+typedef struct {
+	Avl;
+	uchar	*digest;
+} Mtree;
+
+typedef struct Idx Idx;
+struct Idx {
+	Mtree;
+
+	char	*str;			/* as read from idx file */
+	uchar	flags;
+	uvlong	fileid;
+	ulong	lines;
+	ulong	size;
+	ulong	rawbsize;			/* nasty imap4d */
+	ulong	ibadchars;
+
+	char	*ffrom;
+	char	*from;
+	char	*to;
+	char	*cc;
+	char	*bcc;
+	char	*replyto;
+	char	*messageid;
+	char	*subject;
+	char	*sender;
+	char	*inreplyto;
+	char	*date822;
+	char	*idxaux;		/* mailbox specific */
+
+	char	*type;			/* mime info */
+	char	*filename;
+	char	disposition;
+
+	int	nparts;
+};
+
+typedef struct Message Message;
+struct Message {
+	ulong	id;
+	int	refs;
+	int	subname;
+	char	name[12];
+
+	/* top-level indexed information */
+	Idx;
+
+	/* caching help */
+	uchar	cstate;
+	ulong	infolen;
+	ulong	csize;
+
+	/*
+	 * a plethoria of pointers into message
+	 * and some status.  not valid unless cached
+	 */
+	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	mallocd;		/* message is malloc'd */
+	char	ballocd;		/* body is malloc'd */
+	char	hallocd;		/* header is malloc'd */
+	int	badchars;	/* running count of bad chars. */
+
+	char	deleted;
+	char	inmbox;
+
+	/* mail info */
+	char	*unixheader;
+	char	*unixfrom;
+	char	*unixdate;
+	char	*references[Nref]; /* nil terminated unless full */
+
+	/* mime info */
+	char	*charset;		
+	char	*boundary;
+	char	converted;
+	char	encoding;
+	char	decoded;
+
+	Message	*next;
+	Message	*part;
+	Message	*whole;
+	Message	*lru;		/* least recently use chain */
+
+	union{
+		char	*lim;	/* used by plan9; not compatable with cache */
+		vlong	imapuid;	/* used by imap4 */
+		void	*aux;
+	};
+};
+
+typedef struct Mcache Mcache;
+struct Mcache {
+	uvlong	cached;
+	int	nlru;
+	Message	*lru;
+};
+
+typedef struct Mailbox Mailbox;
+struct Mailbox {
+	int	refs;
+	Mailbox	*next;
+	ulong	id;
+	int	flags;
+	char	rmflags;
+	char	dolock;		/* lock when syncing? */
+	char	addfrom;
+	char	name[Elemlen];
+	char	path[Pathlen];
+	Dir	*d;
+	Message	*root;
+	Avltree	*mtree;
+	ulong	vers;		/* goes up each time mailbox is changed */
+
+	/* cache tracking */
+	Mcache;
+
+	/* index tracking */
+	Qid	qid;
+
+	ulong	waketime;
+	void	(*close)(Mailbox*);
+	void	(*decache)(Mailbox*, Message*);
+	char	*(*move)(Mailbox*, Message*, char*);
+	int	(*fetch)(Mailbox*, Message*, uvlong, ulong);
+	void	(*delete)(Mailbox*, Message*);
+	char	*(*ctl)(Mailbox*, int, char**);
+	char	*(*remove)(Mailbox *, int);
+	char	*(*rename)(Mailbox*, char*, int);
+	char	*(*sync)(Mailbox*);
+	void	(*modflags)(Mailbox*, Message*, int);
+	void	(*idxwrite)(Biobuf*, Mailbox*);
+	int	(*idxread)(char*, Mailbox*);
+	void	(*idxinvalid)(Mailbox*);
+	void	*aux;		/* private to Mailbox implementation */
+
+	int	syncing;	/* currently syncing? */
+};
+
+typedef char *Mailboxinit(Mailbox*, char*);
+
+Mailboxinit	plan9mbox;
+Mailboxinit	pop3mbox;
+Mailboxinit	imap4mbox;
+Mailboxinit	mdirmbox;
+
+void		genericidxwrite(Biobuf*, Mailbox*);
+int		genericidxread(char*, Mailbox*);
+void		genericidxinvalid(Mailbox*);
+
+void		cachehash(Mailbox*, Message*);
+int		cacheheaders(Mailbox*, Message*);		/* "getcache" */
+int		cachebody(Mailbox*, Message*);
+int		cacheidx(Mailbox*, Message*);
+int		ensurecache(Mailbox*, Message*);
+
+/**/
+void		putcache(Mailbox*, Message*);		/* asymmetricial */
+void		cachefree(Mailbox*, Message*);
+
+char*		syncmbox(Mailbox*, int);
+void*		emalloc(ulong);
+void*		erealloc(void*, ulong);
+Message*	newmessage(Message*);
+void		unnewmessage(Mailbox*, Message*, Message*);
+char*		delmessages(int, char**);
+char		*flagmessages(int, char**);
+char*		movemessages(int, char**);
+void		digestmessage(Mailbox*, Message*);
+
+int		wraptls(int, char*);
+
+void		eprint(char*, ...);
+void		iprint(char *, ...);
+char*		newmbox(char*, char*, int, Mailbox**);
+void		freembox(char*);
+char*		removembox(char*, int);
+void		syncallmboxes(void);
+void		logmsg(Message*, char*, ...);
+void		msgincref(Mailbox*, Message*);
+void		msgdecref(Mailbox*, Message*);
+void		mboxincref(Mailbox*);
+void		mboxdecref(Mailbox*);
+char		*mboxrename(char*, char*, int);
+void		convert(Message*);
+void		decode(Message*);
+int		decquoted(char*, char*, char*, int);
+int		xtoutf(char*, char**, char*, char*);
+ulong		countlines(Message*);
+void		parse(Mailbox*, Message*, int, int);
+void		parseheaders(Mailbox*, Message*, int, int);
+void		parsebody(Message*, Mailbox*);
+int		strtotm(char*, Tm*);
+char*		lowercase(char*);
+
+char*		sputc(char*, char*, int);
+char*		seappend(char*, char*, char*, int);
+
+int		hdrlen(char*, char*);
+char*		rfc2047(char*, char*, char*, int, int);
+
+char*		localremove(Mailbox*, int);
+char*		localrename(Mailbox*, char*, int);
+void		rmidx(char*, int);
+int		vremove(char*);
+int		rename(char *, char*, int);
+
+void		mtreeinit(Mailbox *);
+void		mtreefree(Mailbox *);
+Message*	mtreefind(Mailbox*, uchar*);
+Message*	mtreeadd(Mailbox*, Message*);
+void		mtreedelete(Mailbox*, Message*);
+
+enum {
+	/* mail sub-objects; must be sorted */
+	Qbcc,
+	Qbody,
+	Qcc,
+	Qdate,
+	Qdigest,
+	Qdisposition,
+	Qffrom,
+	Qfileid,
+	Qfilename,
+	Qflags,
+	Qfrom,
+	Qheader,
+	Qinfo,
+	Qinreplyto,
+	Qlines,
+	Qmessageid,
+	Qmimeheader,
+	Qraw,
+	Qrawbody,
+	Qrawheader,
+	Qrawunix,
+	Qreferences,
+	Qreplyto,
+	Qsender,
+	Qsize,
+	Qsubject,
+	Qto,
+	Qtype,
+	Qunixdate,
+	Qunixheader,
+	Qmax,
+
+	/* other files */
+	Qtop,
+	Qmbox,
+	Qdir,
+	Qctl,
+	Qmboxctl,
+};
+
+#define PATH(id, f)	(((uvlong)(id)<<10) | (f))
+#define FILE(p)		((int) (p) & 0x3ff)
+
+/* hash table to aid in name lookup, all files have an entry */
+typedef struct Hash Hash;
+struct Hash {
+	Hash	*next;
+	char	*name;
+	uvlong	ppath;
+	Qid	qid;
+	Mailbox	*mb;
+	Message	*m;
+};
+
+ulong	hash(char*);
+Hash	*hlook(uvlong, char*);
+void	henter(uvlong, char*, Qid, Message*, Mailbox*);
+void	hfree(uvlong, char*);
+
+char	*intern(char*);
+void	idxfree(Idx*);
+int	rdidxfile(Mailbox*);
+int	wridxfile(Mailbox*);
+
+char	*modflags(Mailbox*, Message*, char*);
+int	getmtokens(char *, char**, int, int);
+
+extern char	Enotme[];
+extern char	*mntpt;
+extern char	user[Elemlen];
+extern char 	*dirtab[];
+extern int	Sflag;
+extern int	iflag;
+extern int	biffing;
+extern ulong	cachetarg;
+extern int	debug;
+extern int	lflag;
+extern int	plumbing;
+extern ulong	msgallocd;
+extern ulong	msgfreed;
+extern int	nocertcheck;
+extern Mailbox	*mbl;
+extern Message	*root;
+extern char	*logf;
+
+#define	dprint(...)	if(debug) fprint(2, __VA_ARGS__); else {}
+#define	Topmsg(mb, m)	(m->whole == mb->root)
+#pragma	varargck	type	"A"	uchar*
+#pragma	varargck	type	"D"	uvlong
+#pragma	varargck	type	"Δ"	uvlong
+#pragma	varargck	argpos	eprint	1
+#pragma	varargck	argpos	iprint	1
+#pragma	varargck	argpos	logmsg	2
+
binary files /dev/null b/sys/src/cmd/upas/fs/extra/fd2path differ
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/fd2path.c
@@ -1,0 +1,32 @@
+#include <u.h>
+#include <libc.h>
+
+void
+usage(void)
+{
+	fprint(2, "usage: fd2path path ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char buf[1024];
+	int fd;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+
+	if(argc == 0){
+		if(fd2path(0, buf, sizeof buf) != -1)
+			fprint(2, "%s\n", buf);
+	}else for(; *argv; argv++){
+		fd = open(*argv, OREAD);
+		if(fd != -1 && fd2path(fd, buf, sizeof buf) != -1)
+			fprint(2, "%s\n", buf);
+		close(fd);
+	}
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/idxtst.c
@@ -1,0 +1,58 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+static char *etab[] = {
+	"not found",
+	"does not exist",
+	"file is locked",
+	"exclusive lock",
+};
+
+static int
+bad(int idx)
+{
+	char buf[ERRMAX];
+	int i;
+
+	rerrstr(buf, sizeof buf);
+	for(i = idx; i < nelem(etab); i++)
+		if(strstr(buf, etab[i]))
+			return 0;
+	return 1;
+}
+
+static int
+exopen(char *s)
+{
+	int i, fd;
+
+	for(i = 0; i < 30; i++){
+		if((fd = open(s, OWRITE|OTRUNC)) >= 0 || bad(0))
+			return fd;
+		if((fd = create(s, OWRITE|OEXCL, DMEXCL|0600)) >= 0  || bad(2))
+			return fd;
+		sleep(1000);
+	}
+	werrstr("lock timeout");
+	return -1;
+}
+
+void
+main(void)
+{
+	int fd;
+	Biobuf *b;
+
+	fd = exopen("testingex");
+	if(fd == -1)
+		sysfatal("exopen: %r");
+	b = Bopen("testingex", OREAD);
+	if(b){
+		free(b);
+		fprint(2, "print both opened at once\n");
+	}else
+		fprint(2, "bopen: %r\n");
+	close(fd);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/infotst.c
@@ -1,0 +1,89 @@
+/*
+ * simulate the read patterns of external programs for testing
+ * info file. "infotest 511 512" simulates what ned does today.
+ *
+ * here's how the new info scheme was verified:
+ *
+	ramfs
+	s=/sys/src/cmd/upas
+	unmount /mail/fs
+	$s/fs/8.out -p
+	for(f in /mail/fs/mbox/*/info){
+		for(i in `{seq 1 1026})
+			$s/fs/infotst $i `{echo $i + 1 | hoc} > /tmp/$i < $f
+		for(i in /tmp/*)
+			cmp $i /tmp/1
+		rm /tmp/*
+	}
+
+	# now test for differences with old scheme under
+	# ideal reading conditions
+	for(f in /mail/fs/mbox/*/info){
+		i = `{echo $f | sed 's:/mail/fs/mbox/([^/]+)/info:\1:g'}
+		$s/fs/infotst 2048 > /tmp/$i < $f
+	}
+	unmount /mail/fs
+	upas/fs -p
+	for(f in /mail/fs/mbox/*/info){
+		i = `{echo $f | sed 's:/mail/fs/mbox/([^/]+)/info:\1:g'}
+		$s/fs/infotst 2048 > /tmp/$i.o < $f
+	}
+	for(i in /tmp/*.o)
+		cmp $i `{echo $i | sed 's:\.o$::g'}
+	rm /tmp/*
+ */
+#include <u.h>
+#include <libc.h>
+
+enum{
+	Ntab	= 100,
+};
+
+int	tab[Ntab];
+int	ntab;
+int	largest;
+
+void
+usage(void)
+{
+	fprint(2, "usage: infotest n1 n2 ... nm\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *buf;
+	int i, n;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+	if(argc == 0)
+		usage();
+	for(; *argv; argv++){
+		if(ntab == nelem(tab))
+			break;
+		i = atoi(*argv);
+		if(i > largest)
+			largest = i;
+		tab[ntab++] = i;
+	}
+	buf = malloc(largest);
+	if(!buf)
+		sysfatal("malloc: %r");
+	for(i = 0;; ){
+		switch(n = read(0, buf, tab[i])){
+		case -1:
+			sysfatal("read: %r");
+		case 0:
+			exits("");
+		default:
+			write(1, buf, n);
+			break;
+		}
+		if(i < ntab-1)
+			i++;
+	}
+}
binary files /dev/null b/sys/src/cmd/upas/fs/extra/paw differ
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/paw.c
@@ -1,0 +1,23 @@
+#include<u.h>
+#include<libc.h>
+#include<bio.h>
+
+void
+main(void)
+{
+	char *f[10], *s;
+	vlong sum;
+	Biobuf b;
+
+	sum = 0;
+	Binit(&b, 0, OREAD);
+
+	while(s = Brdstr(&b, '\n', 1)){
+		if(getfields(s, f, nelem(f), 1, " ") > 2)
+			sum += strtoul(f[2], 0, 0);
+		free(s);
+	}
+	Bterm(&b);
+	print("%lld\n", sum);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/prflags.c
@@ -1,0 +1,37 @@
+#include "common.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: prflags\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *f[Fields+1], buf[20], *s;
+	int n;
+	Biobuf b, o;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+	if(argc)
+		usage();
+	Binit(&b, 0, OREAD);
+	Binit(&o, 1, OWRITE);
+
+	for(; s = Brdstr(&b, '\n', 1); free(s)){
+		n = gettokens(s, f, nelem(f), " ");
+		if(n != Fields)
+			continue;
+		if(!strcmp(f[0], "-"))
+			continue;
+		Bprint(&o, "%s\n", flagbuf(buf, strtoul(f[1], 0, 16)));
+	}
+	Bterm(&b);
+	Bterm(&o);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/strtotmtst.c
@@ -1,0 +1,18 @@
+#include "strtotm.c"
+
+void
+main(int argc, char **argv)
+{
+	Tm tm;
+
+	ARGBEGIN{
+	}ARGEND
+
+	tmfmtinstall();
+	for(; *argv; argv++)
+		if(strtotm(*argv, &tm) >= 0)
+			print("%τ\n", tmfmt(&tm, nil));
+		else
+			print("bad\n");
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/extra/tokens.c
@@ -1,0 +1,62 @@
+#include <u.h>
+#include <libc.h>
+
+/* unfortunately, tokenize insists on collapsing multiple seperators */
+static char qsep[] = " \t\r\n";
+
+static char*
+qtoken(char *s, char *sep)
+{
+	int quoting;
+	char *t;
+
+	quoting = 0;
+	t = s;	/* s is output string, t is input string */
+	while(*t!='\0' && (quoting || utfrune(sep, *t)==nil)){
+		if(*t != '\''){
+			*s++ = *t++;
+			continue;
+		}
+		/* *t is a quote */
+		if(!quoting){
+			quoting = 1;
+			t++;
+			continue;
+		}
+		/* quoting and we're on a quote */
+		if(t[1] != '\''){
+			/* end of quoted section; absorb closing quote */
+			t++;
+			quoting = 0;
+			continue;
+		}
+		/* doubled quote; fold one quote into two */
+		t++;
+		*s++ = *t++;
+	}
+	if(*s != '\0'){
+		*s = '\0';
+		if(t == s)
+			t++;
+	}
+	return t;
+}
+
+int
+getmtokens(char *s, char **args, int maxargs, int multiflag)
+{
+	int i;
+
+	for(i = 0; i < maxargs; i++){
+		if(multiflag)
+			while(*s && utfrune(qsep, *s))
+				s++;
+		else if(*s && utfrune(qsep, *s))
+			s++;
+		if(*s == 0)
+			break;
+		args[i] = s;
+		s = qtoken(s, qsep);
+	}
+	return i;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/fs.c
@@ -1,0 +1,1692 @@
+#include "common.h"
+#include <fcall.h>
+#include <libsec.h>
+#include <pool.h>
+#include "dat.h"
+
+typedef struct Fid Fid;
+struct Fid
+{
+	Qid	qid;
+	short	busy;
+	short	open;
+	int	fid;
+	Fid	*next;
+	Mailbox	*mb;
+	Message	*m;
+
+	long	foff;		/* offset/DIRLEN of finger */
+	Message	*fptr;		/* pointer to message at off */
+	int	fvers;		/* mailbox version when finger was saved */
+};
+
+Fid		*newfid(int);
+void		error(char*);
+void		io(void);
+void		*erealloc(void*, ulong);
+void		*emalloc(ulong);
+void		usage(void);
+void		reader(void);
+int		readheader(Message*, char*, int, 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	Ebadargs[] =	"invalid arguments";
+char 	Enotme[] =	"path not served by this file server";
+char	Eio[] =		"I/O error";
+
+char *dirtab[] = {
+[Qdir]		".",
+[Qbcc]		"bcc",
+[Qbody]		"body",
+[Qcc]		"cc",
+[Qdate]		"date",
+[Qdigest]	"digest",
+[Qdisposition]	"disposition",
+[Qffrom]		"ffrom",
+[Qfileid]		"fileid",
+[Qfilename]	"filename",
+[Qflags]		"flags",
+[Qfrom]		"from",
+[Qheader]	"header",
+[Qinfo]		"info",
+[Qinreplyto]	"inreplyto",
+[Qlines]		"lines",
+[Qmessageid]	"messageid",
+[Qmimeheader]	"mimeheader",
+[Qraw]		"raw",
+[Qrawbody]	"rawbody",
+[Qrawheader]	"rawheader",
+[Qrawunix]	"rawunix",
+[Qreferences]	"references",
+[Qreplyto]	"replyto",
+[Qsender]	"sender",
+[Qsize]		"size",
+[Qsubject]	"subject",
+[Qto]		"to",
+[Qtype]		"type",
+[Qunixdate]	"unixdate",
+[Qunixheader]	"unixheader",
+[Qctl]		"ctl",
+[Qmboxctl]	"ctl",
+};
+
+char	*mntpt;
+char	user[Elemlen];
+int	Dflag;
+int	Sflag;
+int	iflag;
+int	lflag;
+int	biffing;
+int	debug;
+int	plumbing = 1;
+ulong	cachetarg = Maxcache;
+int	nocertcheck; /* ignore unrecognized certs. Still logged */
+Mailbox	*mbl;
+
+static	int	messagesize = 8*1024 + IOHDRSZ;
+static	int	mfd[2];
+static	char	hbuf[32*1024];
+static	uchar	mbuf[16*1024 + IOHDRSZ];
+static	uchar	mdata[16*1024 + IOHDRSZ];
+static	ulong	path;		/* incremented for each new file */
+static	Hash	*htab[2053];
+static	Fcall	rhdr;
+static	Fcall	thdr;
+static	Fid	*fids;
+static QLock	synclock;
+
+void
+sanemsg(Message *m)
+{
+	if(m->end < m->start)
+		abort();
+	if(m->ballocd && (m->start <= m->body && m->end >= m->body))
+		abort();
+	if(m->end - m->start > Maxmsg)
+		abort();
+	if(m->size > Maxmsg)
+		abort();
+	if(m->fileid != 0 && m->fileid <= 1000000ull<<8)
+		abort();
+}
+
+void
+sanembmsg(Mailbox *mb, Message *m)
+{
+	sanemsg(m);
+	if(Topmsg(mb, m)){
+		if(m->start > end && m->size == 0)
+			abort();
+		if(m->fileid <= 1000000ull<<8)
+			abort();
+	}
+}
+
+static int
+Afmt(Fmt *f)
+{
+	char buf[SHA1dlen*2 + 1];
+	uchar *u, i;
+
+	u = va_arg(f->args, uchar*);
+	if(u == 0 && f->flags & FmtSharp)
+		return fmtstrcpy(f, "-");
+	if(u == 0)
+		return fmtstrcpy(f, "<nildigest>");
+	for(i = 0; i < SHA1dlen; i++)
+		sprint(buf + 2*i, "%2.2ux", u[i]);
+	return fmtstrcpy(f, buf);
+}
+
+static int
+Δfmt(Fmt *f)
+{
+	uvlong v;
+	Tm tm;
+
+	v = va_arg(f->args, uvlong);
+	if(f->flags & FmtSharp)
+		if((v>>8) == 0)
+			return fmtstrcpy(f, "");
+	tmtime(&tm, v>>8, tzload("local"));
+	return fmtprint(f, "%τ", tmfmt(&tm, "WW MMM _D hh:mm:ss Z YYYY"));
+}
+
+static int
+Dfmt(Fmt *f)
+{
+	char buf[32];
+	int seq;
+	uvlong v;
+
+	v = va_arg(f->args, uvlong);
+	seq = v & 0xff;
+	if(seq > 99)
+		seq = 99;
+	snprint(buf, sizeof buf, "%llud.%.2d", v>>8, seq);
+	return fmtstrcpy(f, buf);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: upas/fs [-CDSbdlmnps] [-c cachetarg] [-f mboxfile] [-m mountpoint]\n");
+	exits("usage");
+}
+
+void
+notifyf(void *, char *s)
+{
+	if(strncmp(s, "interrupt", 9) == 0)
+		noted(NCONT);
+	if(strncmp(s, "die: yankee pig dog", 19) != 0)
+		/* don't want to call syslog from notify handler */
+		fprint(2, "upas/fs: user: %s; note: %s\n", getuser(), s);
+	noted(NDFLT);
+}
+
+void
+setname(char **v)
+{
+	char buf[128], *p, *e;
+	int fd, i;
+
+	snprint(buf, sizeof buf, "/proc/%d/args", getpid());
+	if((fd = open(buf, OWRITE)) < 0)
+		return;
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s", v[0]);
+	for(i = 0; v[++i]; )
+		p = seprint(p, e, " %s", v[i]);
+	write(fd, buf, p - buf);
+	close(fd);
+}
+
+ulong
+ntoi(char *s)
+{
+	ulong n;
+
+	n = strtoul(s, &s, 0);
+	for(;;)
+	switch(*s++){
+	default:
+		usage();
+	case 'g':
+		n *= 1024;
+	case 'm':
+		n *= 1024;
+	case 'k':
+		n *= 1024;
+		break;
+	case 0:
+		return n;
+	}
+}
+
+void
+main(int argc, char *argv[])
+{
+	char maildir[Pathlen], mbox[Pathlen], srvfile[64], **v;
+	char *mboxfile, *err;
+	int p[2], nodflt, srvpost;
+
+	rfork(RFNOTEG);
+	mboxfile = nil;
+	nodflt = 0;
+	srvpost = 0;
+	v = argv;
+
+	ARGBEGIN{
+	case 'C':
+		nocertcheck = 1;
+		break;
+	case 'D':
+		Dflag = 1;
+		break;
+	case 'S':
+		Sflag = 1;
+		break;
+	case 'b':
+		biffing = 1;
+		break;
+	case 'c':
+		cachetarg = ntoi(EARGF(usage()));
+		break;
+	case 'd':
+		if(++debug > 1)
+			mainmem->flags |= POOL_PARANOIA;
+		break;
+	case 'f':
+		mboxfile = EARGF(usage());
+		break;
+	case 'i':
+		iflag++;
+		break;
+	case 'l':
+		lflag = 1;
+		break;
+	case 'm':
+		mntpt = EARGF(usage());
+		break;
+	case 'n':
+		nodflt = 1;
+		break;
+	case 'p':
+		plumbing = 0;
+		break;
+	case 's':
+		srvpost = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc)
+		usage();
+	fmtinstall('A', Afmt);
+	fmtinstall('D', Dfmt);
+	fmtinstall(L'Δ', Δfmt);
+	fmtinstall('F', fcallfmt);
+	fmtinstall('H', encodefmt);		/* forces tls stuff */
+	tmfmtinstall();
+	quotefmtinstall();
+	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;
+	}
+
+	if(mboxfile != nil)
+		if(err = newmbox(mboxfile, "mbox", 0, nil))
+			sysfatal("opening %s: %s", mboxfile, err);
+
+	switch(rfork(RFFDG|RFPROC|RFNAMEG|RFNOTEG|RFREND)){
+	case -1:
+		error("fork");
+	case 0:
+		henter(PATH(0, Qtop), dirtab[Qctl],
+			(Qid){PATH(0, Qctl), 0, QTFILE}, nil, nil);
+		close(p[1]);
+		setname(v);
+		io();
+		syncallmboxes();
+		syskillpg(getpid());
+		break;
+	default:
+		close(p[0]);	/* don't deadlock if child fails */
+		if(srvpost){
+			snprint(srvfile, sizeof srvfile, "/srv/upasfs.%s", user);
+			post(srvfile, "upasfs", p[1]);
+		}else
+			if(mount(p[1], -1, mntpt, MREPL, "") == -1)
+				error("mount failed");
+	}
+	exits("");
+}
+
+char*
+sputc(char *p, char *e, int c)
+{
+	if(p < e - 1)
+		*p++ = c;
+	return p;
+}
+
+char*
+seappend(char *s, char *e, char *a, int n)
+{
+	int l;
+
+	l = e - s - 1;
+	if(l < n)
+		n = l;
+	memcpy(s, a, n);
+	s += n;
+	*s = 0;
+	return s;
+}
+
+static int
+fileinfo(Mailbox *mb, Message *m, int t, char **pp)
+{
+	char *s, *e, *p;
+	int len, i;
+	static char buf[64 + 512];
+
+	if(cacheidx(mb, m) < 0)
+		return -1;
+	sanembmsg(mb, m);
+	p = nil;
+	len = -1;
+	switch(t){
+	case Qbody:
+		if(cachebody(mb, m) < 0)
+			return -1;
+		p = m->body;
+		len = m->bend - p;
+		break;
+	case Qbcc:
+		p = m->bcc;
+		break;
+	case Qcc:
+		p = m->cc;
+		break;
+	case Qdisposition:
+		switch(m->disposition){
+		case Dinline:
+			p = "inline";
+			break;
+		case Dfile:
+			p = "file";
+			break;
+		}
+		break;
+	case Qdate:
+		if((p = m->date822) != nil)
+			break;
+		/* wet floor */
+	case Qunixdate:
+		p = buf;
+		len = snprint(buf, sizeof buf, "%#Δ", m->fileid);
+		break;
+	case Qfilename:
+		p = m->filename;
+		break;
+	case Qflags:
+		p = flagbuf(buf, m->flags);
+		break;
+	case Qinreplyto:
+		p = m->inreplyto;
+		break;
+	case Qmessageid:
+		p = m->messageid;
+		break;
+	case Qfrom:
+		p = m->from;
+		break;
+	case Qffrom:
+		p = m->ffrom;
+		break;
+	case Qlines:
+		len = snprint(buf, sizeof buf, "%lud", m->lines);
+		p = buf;
+		break;
+	case Qraw:
+		if(cachebody(mb, m) < 0)
+			return -1;
+		p = m->start;
+		if(p != nil)
+		if(strncmp(p, "From ", 5) == 0)
+		if(e = strchr(p, '\n'))
+			p = e + 1;
+		len = m->rbend - p;
+		break;
+	case Qrawunix:
+		if(cachebody(mb, m) < 0)
+			return -1;
+		p = m->start;
+		len = m->end - p;
+		break;
+	case Qrawbody:
+		if(cachebody(mb, m) < 0)
+			return -1;
+		p = m->rbody;
+		len = m->rbend - p;
+		break;
+	case Qrawheader:
+		if(cacheheaders(mb, m) < 0)
+			return -1;
+		p = m->header;
+		len = m->hend - p;
+		break;
+	case Qmimeheader:
+		if(cacheheaders(mb, m) < 0)
+			return -1;
+		p = m->mheader;
+		len = m->mhend - p;
+		break;
+	case Qreferences:
+		if(cacheheaders(mb, m) < 0)
+			return -1;
+		e = buf + sizeof buf;
+		s = buf;
+		for(i = 0; i < nelem(m->references); i++){
+			if(m->references[i] == nil)
+				break;
+			s = seprint(s, e, "%s\n", m->references[i]);
+		}
+		*s = 0;
+		p = buf;
+		len = s - buf;
+		break;
+	case Qreplyto:
+		p = m->replyto;
+		break;
+	case Qsender:
+		p = m->sender;
+		break;
+	case Qsubject:
+		p = m->subject;
+		break;
+	case Qsize:
+		len = snprint(buf, sizeof buf, "%lud", m->size);
+		p = buf;
+		break;
+	case Qto:
+		p = m->to;
+		break;
+	case Qtype:
+		p = m->type;
+		break;
+	case Qfileid:
+		p = buf;
+		len = snprint(buf, sizeof buf, "%D", m->fileid);
+		break;
+	case Qunixheader:
+		if(cacheheaders(mb, m) < 0)
+			return -1;
+		p = m->unixheader;
+		break;
+	case Qdigest:
+		p = buf;
+		len = snprint(buf, sizeof buf, "%A", m->digest);
+		break;
+	}
+	if(p == nil)
+		p = "";
+	if(len == -1)
+		len = strlen(p);
+	*pp = p;
+	putcache(mb, m);
+	return len;
+}
+
+int infofields[] = {
+	Qfrom,
+	Qto,
+	Qcc,
+	Qreplyto,
+	Qunixdate,
+	Qsubject,
+	Qtype,
+	Qdisposition,
+	Qfilename,
+	Qdigest,
+	Qbcc,
+	Qinreplyto,
+	Qdate,
+	Qsender,
+	Qmessageid,
+	Qlines,
+	Qsize,
+	Qflags,
+	Qfileid,
+	Qffrom,
+};
+
+int
+readinfo(Mailbox *mb, Message *m, char *buf, long off, int count)
+{
+	char *s, *p, *e;
+	int i, n;
+	long off0;
+
+	if(m->infolen > 0 && off >= m->infolen)
+		return 0;
+	off0 = off;
+	s = buf;
+	e = s + count;
+	for(i = 0; s < e; i++){
+		if(i == nelem(infofields)){
+			m->infolen = s - buf + off0;
+			break;
+		}
+		if((n = fileinfo(mb, m, infofields[i], &p)) < 0)
+			return -1;
+		if(off > n){
+			off -= n + 1;
+			continue;
+		}
+		if(off){
+			n -= off;
+			p += off;
+			off = 0;
+		}
+		if(s + n > e)
+			n = e - s;
+		memcpy(s, p, n);
+		s += n;
+		if(s < e)
+			*s++ = '\n';
+	}
+	return s - buf;
+}
+
+static int
+mkstat(Dir *d, Mailbox *mb, Message *m, int t)
+{
+	char *p, *e;
+	int n;
+
+	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(m && m->fileid > 1000000ull)
+		d->atime = m->fileid >> 8;
+	else if(mb && mb->d)
+		d->atime = 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 Qheader:
+		if(cacheheaders(mb, m) < 0)
+			return -1;
+		d->name = dirtab[t];
+		d->length = readheader(m, hbuf, 0, sizeof hbuf);
+		putcache(mb, m);
+		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:
+		if((n = readinfo(mb, m, hbuf, 0, sizeof hbuf)) < 0)
+			return -1;
+		d->name = dirtab[t];
+		d->length = n;
+		d->qid.path = PATH(m->id, t);
+		break;
+	case Qraw:
+		if(cacheheaders(mb, m) < 0)
+			return -1;
+		d->name = dirtab[t];
+		d->length = m->size;
+		p = m->start;
+		if(p != nil)
+		if(strncmp(p, "From ", 5) == 0)
+		if(e = strchr(p, '\n'))
+			d->length -= ++e - p;
+		putcache(mb, m);
+		break;
+	case Qrawbody:
+		d->name = dirtab[t];
+		d->length = m->rawbsize;
+		break;
+	case Qrawunix:
+		d->name = dirtab[t];
+		d->length = m->size;
+		if(mb->addfrom && Topmsg(mb, m)){
+			if(cacheheaders(mb, m) < 0)
+				return -1;
+			d->length += strlen(m->unixheader);
+			putcache(mb, m);
+		}
+		break;
+	case Qflags:
+		d->mode = 0666;
+	default:
+		if((n = fileinfo(mb, m, t, &p)) < 0)
+			return -1;
+		d->name = dirtab[t];
+		d->length = n;
+		d->qid.path = PATH(m->id, t);
+		break;
+	}
+	return 0;
+}
+
+char*
+rversion(Fid*)
+{
+	Fid *f;
+
+	if(thdr.msize < 256)
+		return "max messagesize too small";
+	if(thdr.msize < messagesize)
+		messagesize = thdr.msize;
+	rhdr.msize = messagesize;
+	rhdr.version = "9P2000";
+	if(strncmp(thdr.version, "9P", 2) != 0)
+		rhdr.version = "unknown";
+		
+	for(f = fids; f; f = f->next)
+		if(f->busy)
+			rclunk(f);
+	return nil;
+}
+
+char*
+rauth(Fid*)
+{
+	return Enoauth;
+}
+
+char*
+rflush(Fid*)
+{
+	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;
+	if(nf->mb = f->mb)
+		mboxincref(nf->mb);
+	if(nf->m = f->m)
+		msgincref(nf->mb, nf->m);
+	nf->qid = f->qid;
+	return nf;
+}
+
+/* slow?  binary search? */
+static int
+dindex(char *name)
+{
+	int i;
+
+	for(i = 0; i < Qmax; i++)
+		if(dirtab[i] != nil)
+		if(strcmp(dirtab[i], name) == 0)
+			return i;
+	return -1;
+}
+
+char*
+dowalk(Fid *f, char *name)
+{
+	char *p;
+	Hash *h;
+	int t;
+
+	if(f->qid.type != QTDIR)
+		return Enotdir;
+	t = FILE(f->qid.path);
+	if(strcmp(name, ".") == 0)
+		return nil; 
+	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;
+			mboxdecref(f->mb);
+			f->mb = nil;
+			break;
+		case Qdir:
+			if(Topmsg(f->mb, f->m)){
+				f->qid.path = PATH(f->mb->id, Qmbox);
+				f->qid.type = QTDIR;
+				f->qid.vers = f->mb->vers;
+				msgdecref(f->mb, f->m);
+				f->m = nil;
+			} else {
+				msgincref(f->mb, f->m->whole);
+				msgdecref(f->mb, f->m);
+				f->m = f->m->whole;
+				f->qid.path = PATH(f->m->id, Qdir);
+				f->qid.type = QTDIR;
+			}
+			break;
+		}
+		return nil;
+	}
+
+	/* this must catch everything except . and .. */
+	if(t == Qdir && *name >= 'a' && *name <= 'z'){
+		for(;;){
+			t = dindex(name);
+			if(t == -1){
+				if((p = strchr(name, '.')) != nil && *name != '.'){
+					*p = 0;
+					continue;
+				}
+				return Enotexist;
+			}
+			break;
+		}
+		h = hlook(f->qid.path, "xxx");		/* sleezy speedup */
+	} else {
+		h = hlook(f->qid.path, name);
+	}
+
+	if(h == nil)
+		return Enotexist;
+
+	if(h->mb)
+		mboxincref(h->mb);
+	if(h->m)
+		msgincref(h->mb, h->m);
+	if(f->m)
+		msgdecref(f->mb, f->m);
+	if(f->mb)
+		mboxdecref(f->mb);
+	f->m = h->m;
+	f->mb = h->mb;
+	f->qid = h->qid;
+	if(t < Qmax)
+		f->qid.path = PATH(f->m->id, t);	/* sleezy speedup */
+	return nil;
+}
+
+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 && file != Qflags)
+			return Eperm;
+
+	/* make sure we've decoded */
+	if(file == Qbody){
+		if(cachebody(f->mb, f->m) < 0)
+			return Eio;
+		decode(f->m);
+		convert(f->m);
+		putcache(f->mb, f->m);
+	}
+
+	rhdr.iounit = 0;
+	rhdr.qid = f->qid;
+	f->open = 1;
+	return 0;
+}
+
+char*
+rcreate(Fid*)
+{
+	return Eperm;
+}
+
+int
+readtopdir(Fid*, 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 n;
+		n += m;
+		cnt -= m;
+	}
+	pos += m;
+		
+	for(mb = mbl; mb != nil; mb = mb->next){
+		assert(mb->refs > 0);
+
+		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;
+
+	assert(f->mb->refs > 0);
+
+	if(off == 0)
+		syncmbox(f->mb, 1);
+
+	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 n;
+			}
+			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;
+		if(mkstat(&d, f->mb, msg, Qdir) < 0)
+			continue;
+		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++){
+		if(mkstat(&d, f->mb, f->m, i) < 0)
+			continue;
+		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){
+		if(mkstat(&d, f->mb, msg, Qdir) < 0)
+			continue;
+		m = convD2M(&d, &buf[n], blen - n);
+		if(off <= pos){
+			if(m <= BIT16SZ || m > cnt)
+				break;
+			n += m;
+			cnt -= m;
+		}
+		pos += m;
+	}
+	return n;
+}
+
+static int
+mboxctlread(Mailbox *mb, char **p)
+{
+	static char buf[128];
+
+	*p = buf;
+	return snprint(*p, sizeof buf, "%s\n%ld\n", mb->path, mb->vers);
+}
+
+char*
+rread(Fid *f)
+{
+	char *p;
+	int t, i, n, cnt;
+	long off;
+
+	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)
+			n = readtopdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+		else if(t == Qmbox)
+			n = readmboxdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+		else
+			n = readmsgdir(f, mbuf, off, cnt, messagesize - IOHDRSZ);
+		rhdr.count = n;
+		return nil;
+	}
+
+	switch(t){
+	case Qctl:
+		break;
+	case Qmboxctl:
+		i = mboxctlread(f->mb, &p);
+		goto output;
+	case Qheader:
+		if(cacheheaders(f->mb, f->m) < 0)
+			return Eio;
+		rhdr.count = readheader(f->m, (char*)mbuf, off, cnt);
+		putcache(f->mb, f->m);
+		break;
+	case Qinfo:
+		if(cnt > sizeof mbuf)
+			cnt = sizeof mbuf;
+		if((i = readinfo(f->mb, f->m, (char*)mbuf, off, cnt)) < 0)
+			return Eio;
+		rhdr.count = i;
+		break;
+	case Qrawunix:
+		if(f->mb->addfrom && Topmsg(f->mb, f->m)){
+			if(cacheheaders(f->mb, f->m) < 0)
+				return Eio;
+			p = f->m->unixheader;
+			if(off < strlen(p)){
+				rhdr.count = strlen(p + off);
+				memmove(mbuf, p + off, rhdr.count);
+				break;
+			}
+			off -= strlen(p);
+		}
+	default:
+		i = fileinfo(f->mb, f->m, t, &p);
+	output:
+		if(i < 0)
+			return Eio;
+		if(off < i){
+			if(off + cnt > i)
+				cnt = i - off;
+			if(cnt > sizeof mbuf)
+				cnt = sizeof mbuf;
+			memmove(mbuf, p + off, cnt);
+			rhdr.count = cnt;
+		}
+		break;
+	}
+	return nil;
+}
+
+char*
+modflags(Mailbox *mb, Message *m, char *p)
+{
+	char *err;
+	uchar f;
+
+	f = m->flags;
+	if(err = txflags(p, &f))
+		return err;
+	if(f != m->flags){
+		m->flags = f;
+		m->cstate |= Cidxstale;
+		m->cstate |= Cmod;
+		if(mb->modflags != nil)
+			mb->modflags(mb, m, f);
+	}
+	return nil;
+}
+
+char*
+rwrite(Fid *f)
+{
+	char *argvbuf[1024], **argv, file[Pathlen], *err, *v0;
+	int i, t, argc, flags;
+
+	t = FILE(f->qid.path);
+	rhdr.count = thdr.count;
+	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;
+	argv = argvbuf;
+	switch(t){
+	case Qctl:
+		memset(argvbuf, 0, sizeof argvbuf);
+		argc = tokenize(thdr.data, argv, nelem(argvbuf) - 1);
+		if(argc == 0)
+			return Ebadctl;
+		if(strcmp(argv[0], "open") == 0 || strcmp(argv[0], "create") == 0){
+			if(argc == 1 || argc > 3)
+				return Ebadargs;
+			mboxpathbuf(file, sizeof file, getlog(), argv[1]);
+			if(argc == 3){
+				if(strchr(argv[2], '/') != nil)
+					return "/ not allowed in mailbox name";
+			}else
+				argv[2] = nil;
+			flags = 0;
+			if(strcmp(argv[0], "create") == 0)
+				flags |= DMcreate;
+			return newmbox(file, argv[2], flags, nil);
+		}
+		if(strcmp(argv[0], "close") == 0){
+			if(argc < 2)
+				return nil;
+			for(i = 1; i < argc; i++)
+				freembox(argv[i]);
+			return nil;
+		}
+		if(strcmp(argv[0], "delete") == 0){
+			if(argc < 3)
+				return nil;
+			delmessages(argc - 1, argv + 1);
+			return nil;
+		}
+		if(strcmp(argv[0], "flag") == 0){
+			if(argc < 3)
+				return nil;
+			return flagmessages(argc - 1, argv + 1);
+		}
+		if(strcmp(argv[0], "move") == 0){
+			if(argc < 4)
+				return nil;
+			return movemessages(argc - 1, argv + 1);
+		}
+		if(strcmp(argv[0], "remove") == 0){
+			v0 = argv0;
+			flags = 0;
+			ARGBEGIN{
+			default:
+				argv0 = v0;
+				return Ebadargs;
+			case 'r':
+				flags |= Rrecur;
+				break;
+			case 't':
+				flags |= Rtrunc;
+				break;
+			}ARGEND
+			argv0 = v0;
+			if(argc == 0)
+				return Ebadargs;
+			for(; *argv; argv++){
+				mboxpathbuf(file, sizeof file, getlog(), *argv);
+				if(err = newmbox(file, nil, 0, nil))
+					return err;
+				if(err = removembox(file, flags))
+					return err;
+			}
+			return 0;
+		}
+		if(strcmp(argv[0], "rename") == 0){
+			v0 = argv0;
+			flags = 0;
+			ARGBEGIN{
+			case 't':
+				flags |= Rtrunc;
+				break;
+			}ARGEND
+			argv0 = v0;
+			if(argc != 2)
+				return Ebadargs;
+			return mboxrename(argv[0], argv[1], flags);
+		}
+		return Ebadctl;
+	case Qmboxctl:
+		if(f->mb->ctl == nil)
+			break;
+		argc = tokenize(thdr.data, argv, nelem(argvbuf));
+		if(argc == 0)
+			return Ebadctl;
+		return f->mb->ctl(f->mb, argc, argv);
+	case Qflags:
+		/*
+		 * modifying flags on subparts is a little strange.
+		 */
+		if(!Topmsg(f->mb, f->m))
+			break;
+		return modflags(f->mb, f->m, thdr.data);
+	}
+	return Eperm;
+}
+
+char*
+rclunk(Fid *f)
+{
+	f->busy = 1;
+	/* coherence(); */
+	f->fid = -1;
+	f->open = 0;
+	if(f->m != nil){
+		msgdecref(f->mb, f->m);
+		f->m = nil;
+	}
+	if(f->mb != nil){
+		mboxdecref(f->mb);
+		f->mb = nil;
+	}
+	f->busy = 0;
+	return 0;
+}
+
+char *
+rremove(Fid *f)
+{
+	if(f->mb != nil && f->m != nil && Topmsg(f->mb, f->m) && f->m->deleted == 0)
+		f->m->deleted = Deleted;
+	return rclunk(f);
+}
+
+char *
+rstat(Fid *f)
+{
+	Dir d;
+
+	if(FILE(f->qid.path) == Qmbox)
+		syncmbox(f->mb, 1);
+	if(mkstat(&d, f->mb, f->m, FILE(f->qid.path)) < 0)
+		return Eio;
+	rhdr.nstat = convD2M(&d, mbuf, messagesize - IOHDRSZ);
+	rhdr.stat = mbuf;
+	return 0;
+}
+
+char*
+rwstat(Fid*)
+{
+	return Eperm;
+}
+
+static Fid*
+checkfid(Fid *f)
+{
+	if(f->busy)
+	switch(FILE(f->qid.path)){
+	case Qtop:
+	case Qctl:
+		assert(f->mb == nil);
+		assert(f->m == nil);
+		break;
+	case Qmbox:
+	case Qmboxctl:
+		assert(f->mb != nil && f->mb->refs > 0);
+		assert(f->m == nil);
+		break;
+	default:
+		assert(f->mb != nil && f->mb->refs > 0);
+		assert(f->m != nil && f->m->refs > 0);
+		break;
+	}
+	return f;
+}
+
+Fid*
+newfid(int fid)
+{
+	Fid *f, *ff;
+
+	ff = 0;
+	for(f = fids; f; f = f->next)
+		if(f->fid == fid)
+			return checkfid(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;
+}
+
+void
+io(void)
+{
+	char *err;
+	int n;
+
+	/* start a process to watch the mailboxes*/
+	if(plumbing || biffing)
+		switch(rfork(RFPROC|RFMEM)){
+		case -1:
+			/* oh well */
+			break;
+		case 0:
+			reader();
+			exits("");
+		default:
+			break;
+		}
+
+	for(;;){
+		n = read9pmsg(mfd[0], mdata, messagesize);
+		if(n <= 0)
+			return;
+		if(convM2S(mdata, n, &thdr) == 0)
+			continue;
+
+		if(Dflag)
+			fprint(2, "%s:<-%F\n", argv0, &thdr);
+
+		qlock(&synclock);
+		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;
+		qunlock(&synclock);
+
+		if(Dflag)
+			fprint(2, "%s:->%F\n", argv0, &rhdr);
+		n = convS2M(&rhdr, mdata, messagesize);
+		if(write(mfd[1], mdata, n) != n)
+			error("mount write");
+	}
+}
+
+static char *readerargv[] = {"upas/fs", "plumbing", 0};
+
+void
+reader(void)
+{
+	ulong t;
+	Dir *d;
+	Mailbox *mb;
+
+	setname(readerargv);
+	sleep(15*1000);
+	for(;;){
+		qlock(&synclock);
+		t = time(0);
+		for(mb = mbl; mb != nil; mb = mb->next){
+			if(mb->waketime != 0 && t >= mb->waketime){
+				mb->waketime = 0;
+				break;
+			}
+			if(mb->d != nil){
+				d = dirstat(mb->path);
+				if(d != nil){
+					if(d->qid.path != mb->d->qid.path
+					|| d->qid.vers != mb->d->qid.vers){
+						free(d);
+						break;
+					}
+					free(d);
+				}
+			}
+		}
+		if(mb != nil) {
+			syncmbox(mb, 1);
+			qunlock(&synclock);
+		} else {
+			qunlock(&synclock);
+			sleep(15*1000);
+		}
+	}
+}
+
+void
+error(char *s)
+{
+	syskillpg(getpid());
+	eprint("upas/fs: fatal error: %s: %r\n", s);
+	exits(s);
+}
+
+
+typedef struct Ignorance Ignorance;
+struct Ignorance
+{
+	Ignorance *next;
+	char	*str;
+	int	len;
+};
+static Ignorance *ignorance;
+
+/*
+ *  read the file of headers to ignore
+ */
+static 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 = emalloc(sizeof *i);
+		i->len = strlen(p);
+		i->str = strdup(p);
+		if(i->str == 0){
+			free(i);
+			break;
+		}
+		i->next = ignorance;
+		ignorance = i;
+	}
+	Bterm(b);
+}
+
+static int
+ignore(char *p, int n)
+{
+	Ignorance *i;
+
+	readignore();
+	for(i = ignorance; i != nil; i = i->next)
+		if(i->len <= n && cistrncmp(i->str, p, i->len) == 0)
+			return 1;
+	return 0;
+}
+
+int
+readheader(Message *m, char *buf, int off, int cnt)
+{
+	char *s, *end, *se, *p, *e, *to;
+	int n, ns, salloc;
+
+	to = buf;
+	p = m->header;
+	e = m->hend;
+	s = emalloc(salloc = 2048);
+	end = s + salloc;
+
+	/* copy in good headers */
+	while(cnt > 0 && p < e){
+		if((n = hdrlen(p, e)) <= 0)
+			break;
+		if(ignore(p, n)){
+			p += n;
+			continue;
+		}
+		if(n + 1 > salloc){
+			s = erealloc(s, salloc = n + 1);
+			end = s + salloc;
+		}
+		se = rfc2047(s, end, p, n, 0);
+		ns = se - s;
+		if(off > 0){
+			if(ns <= off){
+				off -= ns;
+				p += n;
+				continue;
+			}
+			ns -= off;
+		}
+		if(ns > cnt)
+			ns = cnt;
+		memmove(to, s + off, ns);
+		to += ns;
+		p += n;
+		cnt -= ns;
+		off = 0;
+	}
+	free(s);
+	return to - buf;
+}
+
+ulong
+hash(char *s)
+{
+	ulong c, h;
+
+	h = 0;
+	while(c = *s++)
+		h = h*131 + c;
+
+	return h;
+}
+
+Hash*
+hlook(uvlong ppath, char *name)
+{
+	ulong h;
+	Hash *hp;
+
+	h = (hash(name)+ppath) % nelem(htab);
+	for(hp = htab[h]; hp != nil; hp = hp->next)
+		if(ppath == hp->ppath && strcmp(name, hp->name) == 0)
+			return hp;
+	return nil;
+}
+
+void
+henter(uvlong ppath, char *name, Qid qid, Message *m, Mailbox *mb)
+{
+	ulong h;
+	Hash *hp, **l;
+
+	h = (hash(name)+ppath) % nelem(htab);
+	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;
+			return;
+		}
+	}
+	*l = hp = emalloc(sizeof(*hp));
+	hp->m = m;
+	hp->mb = mb;
+	hp->qid = qid;
+	hp->name = name;
+	hp->ppath = ppath;
+}
+
+void
+hfree(uvlong ppath, char *name)
+{
+	ulong h;
+	Hash *hp, **l;
+
+	h = (hash(name)+ppath) % nelem(htab);
+	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;
+		}
+	}
+}
+
+void
+post(char *name, char *envname, int srvfd)
+{
+	char buf[32];
+	int fd;
+
+	fd = create(name, OWRITE, 0600);
+	if(fd < 0)
+		error("post failed");
+	snprint(buf, sizeof buf, "%d", srvfd);
+	if(write(fd, buf, strlen(buf)) != strlen(buf))
+		error("srv write");
+	close(fd);
+	putenv(envname, name);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/header.c
@@ -1,0 +1,176 @@
+#include "common.h"
+#include <ctype.h>
+#include <libsec.h>
+#include "dat.h"
+
+int
+hdrlen(char *p, char *e)
+{
+	char *ep;
+
+	ep = p;
+	do {
+		ep = memchr(ep, '\n', e - ep);
+		if(ep == nil){
+			ep = e;
+			break;
+		}
+		if(ep == p)
+			break;
+		if(ep - p == 1 && ep[-1] == '\r')
+			break;
+		ep++;
+		if(ep >= e){
+			ep = e;
+			break;
+		}
+	} while(*ep == ' ' || *ep == '\t');
+	return ep - p;
+}
+
+/* rfc2047 non-ascii: =?charset?q?encoded-text?= */
+static int
+tok(char **sp, char *se, char *token, int len)
+{
+	char charset[100], *s, *e, *x;
+	int l;
+
+	if(len == 0)
+		return -1;
+	s = *sp;
+	e = token + len - 2;
+	token += 2;
+
+	x = memchr(token, '?', e - token);
+	if(x == nil || (l = x - token) >= sizeof charset)
+		return -1;
+	memmove(charset, token, l);
+	charset[l] = 0;
+
+	/* bail if it doesn't fit */
+	token = x + 1;
+	if(e - token > se - s - 1)
+		return -1;
+
+	if(cistrncmp(token, "b?", 2) == 0){
+		token += 2;
+		len = dec64((uchar*)s, se - s - 1, token, e - token);
+		if(len == -1)
+			return -1;
+		s[len] = 0;
+	}else if(cistrncmp(token, "q?", 2) == 0){
+		token += 2;
+		len = decquoted(s, token, e, 1);
+		if(len > 0 && s[len - 1] == '\n')
+			len--;
+		s[len] = 0;
+	}else
+		return -1;
+
+	if(xtoutf(charset, &x, s, s + len) <= 0)
+		s += len;
+	else {
+		s = seprint(s, se, "%s", x);
+		free(x);
+	}
+	*sp = s;
+	return 0;
+}
+
+char*
+tokbegin(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;
+}
+
+static char*
+seappend822f(char *s, char *e, char *a, int n)
+{
+	int skip, c;
+
+	skip = 0;
+	for(; n--; a++){
+		c = *a;
+		if(skip && isspace(c))
+			continue;
+		if(c == '\n'){
+			c = ' ';
+			skip = 1;
+		}else{
+			if(c < 0x20)
+				continue;
+			skip = 0;
+		}
+		s = sputc(s, e, c);
+	}
+	return s;
+}
+
+static char*
+seappend822(char *s, char *e, char *a, int n)
+{
+	int c;
+
+	for(; n--; a++){
+		c = *a;
+		if(c < 0x20 && c != '\n' && c != '\t')
+			continue;
+		s = sputc(s, e, c);
+	}
+	return s;
+}
+
+/* convert a header line */
+char*
+rfc2047(char *s, char *se, char *uneaten, int len, int fold)
+{
+	char *sp, *token, *p, *e;
+	char *(*f)(char*, char*, char*, int);
+
+	f = seappend822;
+	if(fold)
+		f = seappend822f;
+	sp = s;
+	p = uneaten;
+	for(e = p + len; p < e; ){
+		while(*p++ == '=' && (token = tokbegin(uneaten, p))){
+			sp = f(sp, se, uneaten, token - uneaten);
+			if(tok(&sp, se, token, p - token) < 0)
+				sp = f(sp, se, token, p - token);
+			uneaten = p;
+			for(; p < e && isspace(*p);)
+				p++;
+			if(p + 2 < e && p[0] == '=' && p[1] == '?')
+				uneaten = p;	/* paste */
+		}
+	}
+	if(p > uneaten)
+		sp = f(sp, se, uneaten, e - uneaten);
+	*sp = 0;
+	return sp;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/idx.c
@@ -1,0 +1,561 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+#define idprint(...)	if(iflag > 1) fprint(2, __VA_ARGS__); else {}
+#define iprint(...)		if(iflag) fprint(2, __VA_ARGS__); else {}
+
+static char *magic	= "idx magic v9\n";
+static char *mbmagic	= "genericv1";
+enum {
+	Idxfields		= 23,
+
+	Idxto		= 30000,		/* index timeout in ms */
+	Idxstep		= 300,		/* sleep between tries */
+};
+
+typedef struct Intern Intern;
+struct Intern
+{
+	Intern	*next;
+	char	str[];
+};
+
+static Intern *itab[64];
+
+char*
+intern(char *s)
+{
+	Intern *i, **h;
+	int n;
+
+	h = &itab[hash(s) % nelem(itab)];
+	for(i = *h; i != nil; i = i->next)
+		if(strcmp(s, i->str) == 0)
+			return i->str;
+	n = strlen(s)+1;
+	i = emalloc(sizeof(*i) + n);
+	memmove(i->str, s, n);
+	i->next = *h;
+	*h = i;
+	return i->str;
+}
+
+void
+idxfree(Idx *i)
+{
+	if(i->str)
+		free(i->str);
+	else{
+		free(i->digest);
+		free(i->ffrom);
+		free(i->from);
+		free(i->to);
+		free(i->cc);
+		free(i->bcc);
+		free(i->replyto);
+		free(i->messageid);
+		free(i->subject);
+		free(i->sender);
+		free(i->inreplyto);
+		free(i->date822);
+		free(i->filename);
+		free(i->idxaux);
+	}
+	memset(i, 0, sizeof *i);
+}
+
+static char*
+∂(char *x)
+{
+	if(x)
+		return x;
+	return "";
+}
+
+static int
+pridxmsg(Biobuf *b, Idx *x)
+{
+	Bprint(b, "%#A %ux %D %lud ", x->digest, x->flags&~Frecent, x->fileid, x->lines);
+	Bprint(b, "%q %q %q %q %q ", ∂(x->ffrom), ∂(x->from), ∂(x->to), ∂(x->cc), ∂(x->bcc));
+	Bprint(b, "%q %q %q %q %q %q ", ∂(x->replyto), ∂(x->messageid), ∂(x->subject), ∂(x->sender), ∂(x->inreplyto), ∂(x->date822));
+	Bprint(b, "%s %d %q %lud %lud ", x->type, x->disposition, ∂(x->filename), x->size, x->rawbsize);
+	Bprint(b, "%lud %q %d\n", x->ibadchars, ∂(x->idxaux), x->nparts);
+	return 0;
+}
+
+static int
+pridx0(Biobuf *b, Mailbox *mb, Message *m, int l)
+{
+	for(; m; m = m->next){
+		if(l == 0)
+		if(ensurecache(mb, m) == -1)
+			continue;
+		if(pridxmsg(b, m))
+			return -1;
+		if(m->part)
+			pridx0(b, mb, m->part, l + 1);
+		m->cstate &= ~Cidxstale;
+		m->cstate |= Cidx;
+		if(l == 0)
+			msgdecref(mb, m);
+	}
+	return 0;
+}
+
+void
+genericidxwrite(Biobuf *b, Mailbox*)
+{
+	Bprint(b, "%s\n", mbmagic);
+}
+
+static int
+pridx(Biobuf *b, Mailbox *mb)
+{
+	int i;
+
+	Bprint(b, magic);
+	mb->idxwrite(b, mb);
+	i = pridx0(b, mb, mb->root->part, 0);
+	return i;
+}
+
+static char *eopen[] = {
+	"not found",
+	"does not exist",
+	0,
+};
+
+static char *ecreate[] = {
+	"already exists",
+	0,
+};
+
+static char *elocked[] = {
+	"file is locked",
+	"file locked",
+	"exclusive lock",
+	0,
+};
+
+static int
+bad(char **t)
+{
+	char buf[ERRMAX];
+	int i;
+
+	rerrstr(buf, sizeof buf);
+	for(i = 0; t[i]; i++)
+		if(strstr(buf, t[i]))
+			return 0;
+	return 1;
+}
+
+static int
+forceexcl(int fd)
+{
+	int r;
+	Dir *d;
+
+	d = dirfstat(fd);
+	if(d == nil)
+		return 0;			/* ignore: assume file removed */
+	if(d->mode & DMEXCL){
+		free(d);
+		return 0;
+	}
+	d->mode |= DMEXCL;
+	d->qid.type |= QTEXCL;
+	r = dirfwstat(fd, d);
+	free(d);
+	if(r == -1)
+		return 0;			/* ignore unwritable (e.g dump) */
+	close(fd);
+	return -1;
+}
+
+static int
+exopen(char *s)
+{
+	int i, fd;
+
+	for(i = 0; i < Idxto/Idxstep; i++){
+		if((fd = open(s, OWRITE|OTRUNC)) >= 0 || (bad(eopen) && bad(elocked))){
+			if(fd != -1 && forceexcl(fd) == -1)
+				continue;
+			return fd;
+		}
+		if((fd = create(s, OWRITE|OEXCL, DMTMP|DMEXCL|0600)) >= 0  || (bad(ecreate) && bad(elocked)))
+			return fd;
+		sleep(Idxstep);
+	}
+	werrstr("lock timeout");
+	return -1;
+}
+
+static Message*
+findmessage(Mailbox *, Message *parent, int n)
+{
+	Message *m;
+
+	for(m = parent->part; m; m = m->next)
+		if(m->digest == nil && n-- == 0)
+			return m;
+	return 0;
+}
+
+static int
+validmessage(Mailbox *mb, Message *m, int level)
+{
+	if(level){
+		if(m->digest != nil)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+		if(m->fileid != 0)
+			goto lose;
+	}else{
+		if(m->digest == nil)
+			goto lose;
+		if(m->size == 0)
+			goto lose;
+		if(m->fileid <= 1000000ull<<8)
+			goto lose;
+		if(mtreefind(mb, m->digest))
+			goto lose;
+	}
+	return 1;
+lose:
+	eprint("invalid cache[%d] %#A size %ld %D\n", level, m->digest, m->size, m->fileid);
+	return 0;
+}
+
+/*
+ * n.b.: we don't ensure this is the index version we last read.
+ *
+ * we may overwrite changes.  dualing deletes should sync eventually.
+ * mboxsync should complain about missing messages but
+ * mutable information (which is not in the email itself)
+ * may be lost.
+ */
+int
+wridxfile(Mailbox *mb)
+{
+	char buf[Pathlen + 4];
+	int r, fd;
+	Biobuf b;
+	Dir *d;
+
+	snprint(buf, sizeof buf, "%s.idx", mb->path);
+	iprint("wridxfile %s\n", buf);
+	if((fd = exopen(buf)) == -1){
+		rerrstr(buf, sizeof buf);
+		if(strcmp(buf, "no creates") != 0)
+		if(strstr(buf, "file system read only") == 0)
+			eprint("wridxfile: %r\n");
+		return -1;
+	}
+	seek(fd, 0, 0);
+	Binit(&b, fd, OWRITE);
+	r = pridx(&b, mb);
+	Bterm(&b);
+	d = dirfstat(fd);
+	if(d == 0)
+		sysfatal("dirfstat: %r");
+	mb->qid = d->qid;
+	free(d);
+	close(fd);
+	return r;
+}
+
+static int
+nibble(int c)
+{
+	if(c >= '0' && c <= '9')
+		return c - '0';
+	if(c < 0x20)
+		c += 0x20;
+	if(c >= 'a' && c <= 'f')
+		return c - 'a'+10;
+	return 0xff;
+}
+
+static uchar*
+hackdigest(char *s)
+{
+	int i;
+
+	if(strcmp(s, "-") == 0)
+		return nil;
+	if(strlen(s) != 2*SHA1dlen){
+		eprint("bad digest %s\n", s);
+		return nil;
+	}
+	for(i = 0; i < SHA1dlen; i++)
+		((uchar*)s)[i] = nibble(s[2*i])<<4 | nibble(s[2*i + 1]);
+	s[i] = 0;
+	return (uchar*)s;
+}
+
+static uvlong
+rdfileid(char *s, int level)
+{
+	char *p;
+	uvlong uv;
+
+	uv = strtoul(s, &p, 0);
+	if((level == 0 && uv < 1000000) || *p != '.')
+		return 0;
+	return uv<<8 | strtoul(p + 1, 0, 10);
+}
+
+static char*
+∫(char *x)
+{
+	if(x && *x)
+		return x;
+	return nil;
+}
+
+/*
+ * strategy:  use top-level avl tree to merge index with
+ * our ideas about the mailbox.  new or old messages
+ * with corrupt index entries are marked Dead.  they
+ * will be cleared out of the mailbox and are kept out
+ * of the index.  when messages are marked Dead, a
+ * reread of the mailbox is forced.
+ *
+ * side note.  if we get a new message while we are
+ * running it is added to the list in order but m->id
+ * looks out-of-order.  this is because m->id must
+ * increase monotonicly.  a new instance of the fs
+ * will result in a different ordering.
+ */
+
+static int
+rdidx(Biobuf *b, Mailbox *mb, Message *parent, int npart, int level)
+{
+	char *f[Idxfields + 1], *s;
+	uchar *digest;
+	int n, flags, nparts, good, bad, redux;
+	Message *m, **ll, *l;
+
+	bad = good = redux = 0;
+	ll = &parent->part;
+	nparts = npart;
+	for(; npart != 0 && (s = Brdstr(b, '\n', 1)); npart--){
+		m = nil;
+		digest = nil;
+		n = tokenize(s, f, nelem(f));
+		if(n != Idxfields){
+dead:
+			eprint("bad index %#A %d %d n=%d\n", digest, level, npart, n);
+			bad++;
+			free(s);
+			if(level)
+				return -1;
+			if(m)
+				m->deleted = Dead;
+			continue;
+		}
+		digest = hackdigest(f[0]);
+		if(level == 0){
+			if(digest == nil)
+				goto dead;
+			m = mtreefind(mb, digest);
+		} else
+			m = findmessage(mb, parent, nparts - npart);
+		if(m){
+			/*
+			 * read in mutable information.
+			 * currently this is only flags
+			 * and nparts.
+			 */
+			redux++;
+			if(level == 0)
+				m->deleted &= ~Dmark;
+			n = m->nparts;
+			m->nparts = strtoul(f[22], 0, 0);
+			if(rdidx(b, mb, m, m->nparts, level + 1) == -1)
+				goto dead;
+			ll = &m->next;
+			idprint("%d seen before %lud... %.2ux", level, m->id, m->cstate);
+			flags = m->flags;
+			m->flags |= strtoul(f[1], 0, 16);
+			if(flags != m->flags || n != m->nparts)
+				m->cstate |= Cidxstale;
+			m->cstate |= Cidx;
+			idprint("→%.2ux\n", m->cstate);
+			free(s);
+			continue;
+		}
+		m = newmessage(parent);
+		idprint("%d new %lud %#A\n", level, m->id, digest);
+		m->digest = digest;
+		m->flags = strtoul(f[1], 0, 16);
+		m->fileid = rdfileid(f[2], level);
+		m->lines = atoi(f[3]);
+		m->ffrom = ∫(f[4]);
+		m->from = ∫(f[5]);
+		m->to = ∫(f[6]);
+		m->cc = ∫(f[7]);
+		m->bcc = ∫(f[8]);
+		m->replyto = ∫(f[9]);
+		m->messageid = ∫(f[10]);
+		m->subject = ∫(f[11]);
+		m->sender = ∫(f[12]);
+		m->inreplyto = ∫(f[13]);
+		m->date822 = ∫(f[14]);
+		m->type = intern(f[15]);
+		m->disposition = atoi(f[16]);
+		m->filename = ∫(f[17]);
+		m->size = strtoul(f[18], 0, 0);
+		m->rawbsize = strtoul(f[19], 0, 0);
+		m->ibadchars = strtoul(f[20], 0, 0);
+		m->idxaux = ∫(f[21]);
+		m->nparts = strtoul(f[22], 0, 0);
+		m->cstate &= ~Cidxstale;
+		m->cstate |= Cidx|Cnew;
+		m->str = s;
+		s = 0;
+
+		if(!validmessage(mb, m, level))
+			goto dead;
+		if(level == 0){
+			mtreeadd(mb, m);
+			m->inmbox = 1;
+		}
+		cachehash(mb, m);		/* hokey */
+		l = *ll;
+		*ll = m;
+		ll = &m->next;
+		*ll = l;
+		good++;
+
+		if(m->nparts)
+		if(rdidx(b, mb, m, m->nparts, level + 1) == -1)
+			goto dead;
+	}
+	if(level == 0 && bad + redux > 0)
+		iprint("idx: %d %d %d\n", good, bad, redux);
+	if(bad)
+		return -1;
+	return 0;
+}
+
+/* bug: should check time. */
+static int
+qidcmp(int fd, Qid *q)
+{
+	int r;
+	Dir *d;
+	Qid q0;
+
+	d = dirfstat(fd);
+	if(!d)
+		sysfatal("dirfstat: %r");
+	r = 1;
+	if(d->qid.path == q->path)
+	if(d->qid.vers == q->vers)
+		r = 0;
+	q0 = *q;
+	*q = d->qid;
+	free(d);
+	if(q0.path != 0 && r)
+		iprint("qidcmp ... index changed [%ld .. %ld]\n", q0.vers, q->vers);
+	return r;
+}
+
+static int
+verscmp(Biobuf *b, Mailbox *mb)
+{
+	char *s;
+	int n;
+
+	n = -1;
+	if(s = Brdstr(b, '\n', 0))
+		n = strcmp(s, magic);
+	free(s);
+	if(n)
+		return -1;
+	n = -1;
+	if(s = Brdstr(b, '\n', 0))
+		n = mb->idxread(s, mb);
+	free(s);
+	return n;
+}
+
+int
+genericidxread(char *s, Mailbox*)
+{
+	return strcmp(s, mbmagic);
+}
+
+void
+genericidxinvalid(Mailbox *mb)
+{
+	if(mb->d)
+		memset(&mb->d->qid, 0, sizeof mb->d->qid);
+	mb->waketime = time(0);
+}
+
+void
+mark(Mailbox *mb)
+{
+	Message *m;
+
+	for(m = mb->root->part; m != nil; m = m->next)
+		m->deleted |= Dmark;
+}
+
+int
+unmark(Mailbox *mb)
+{
+	int i;
+	Message *m;
+
+	i = 0;
+	for(m = mb->root->part; m != nil; m = m->next)
+		if(m->deleted & Dmark){
+			i++;
+			m->deleted &= ~Dmark;	/* let mailbox scan figure this out.  BOTCH?? */
+		}
+	return i;
+}
+
+int
+rdidxfile0(Mailbox *mb)
+{
+	char buf[Pathlen + 4];
+	int r, v;
+	Biobuf *b;
+
+	snprint(buf, sizeof buf, "%s.idx", mb->path);
+	while((b = Bopen(buf, OREAD)) == nil && !bad(elocked))
+		sleep(1000);
+	if(b == nil)
+		return -2;
+	if(qidcmp(Bfildes(b), &mb->qid) == 0)
+		r = 0;
+	else if(verscmp(b, mb) == -1)
+		r = -1;
+	else{
+		mark(mb);
+		r = rdidx(b, mb, mb->root, -1, 0);
+		v = unmark(mb);
+		if(r == 0 && v > 0)
+			r = -1;
+	}
+	Bterm(b);
+	return r;
+}
+
+int
+rdidxfile(Mailbox *mb)
+{
+	int r;
+
+	r = rdidxfile0(mb);
+	if(r == -1 && mb->idxinvalid)
+		mb->idxinvalid(mb);
+	return r;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/imap.c
@@ -1,0 +1,1211 @@
+/*
+ * todo:
+ * 1.	sync with imap server's flags
+ * 2.	better algorithm for avoiding downloading message list.
+ * 3.	get sender — eating envelope is lots of work!
+ */
+#include "common.h"
+#include <libsec.h>
+#include <auth.h>
+#include "dat.h"
+
+#define	idprint(i, ...)	if(i->flags & Fdebug) fprint(2, __VA_ARGS__); else {}
+#pragma varargck argpos	imap4cmd	2
+#pragma varargck	type	"Z"		char*
+#pragma varargck	type	"U"		uvlong
+#pragma varargck	type	"U"		vlong
+
+static char	confused[]	= "confused about fetch response";
+static char	qsep[]		= " \t\r\n";
+static char	Eimap4ctl[]	= "bad imap4 control message";
+
+enum{
+	/* cap */
+	Cnolog	= 1<<0,
+	Ccram	= 1<<1,
+	Cntlm	= 1<<2,
+
+	/* flags */
+	Fssl	= 1<<0,
+	Fdebug	= 1<<1,
+	Fgmail	= 1<<2,
+};
+
+typedef struct {
+	uvlong	uid;
+	ulong	sizes;
+	ulong	dates;
+	uint	flags;
+} Fetchi;
+
+typedef struct Imap Imap;
+struct Imap {
+	char	*mbox;
+	/* free this to free the strings below */
+	char	*freep;
+	char	*host;
+	char	*user;
+
+	int	refreshtime;
+	uchar	cap;
+	uchar	flags;
+
+	ulong	tag;
+	ulong	validity;
+	ulong	newvalidity;
+	int	nmsg;
+	int	size;
+
+	/*
+	 * These variables are how we keep track
+	 * of what's been added or deleted. They
+	 * keep a count of the number of uids we
+	 * have processed this sync (nuid), and
+	 * the number we processed last sync
+	 * (muid).
+	 *
+	 * We keep the latest imap state in fetchi,
+	 * and imap4read syncs the information in
+	 * it with the messages. That's how we know
+	 * something changed on the server.
+	 */
+	Fetchi	*f;
+	int	nuid;
+	int	muid;
+
+	/* open network connection */
+	Biobuf	bin;
+	Biobuf	bout;
+	int	binit;
+	int	fd;
+};
+
+enum
+{
+	Qok = 0,
+	Qquote,
+	Qbackslash,
+};
+
+static int
+needtoquote(Rune r)
+{
+	if(r >= Runeself)
+		return Qquote;
+	if(r <= ' ')
+		return Qquote;
+	if(r == '(' || r == ')' || r == '{' || r == '%' || r == '*' || r == ']')
+		return Qquote;
+	if(r == '\\' || r == '"')
+		return Qbackslash;
+	return Qok;
+}
+
+static int
+Zfmt(Fmt *f)
+{
+	char *s, *t;
+	int w, quotes;
+	Rune r;
+
+	s = va_arg(f->args, char*);
+	if(s == 0 || *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, '"');
+}
+
+static int
+Ufmt(Fmt *f)
+{
+	char buf[20*2 + 2];
+	ulong a, b;
+	uvlong u;
+
+	u = va_arg(f->args, uvlong);
+	if(u == 1)
+		return fmtstrcpy(f, "nil");
+	if(u == 0)
+		return fmtstrcpy(f, "-");
+	a = u>>32;
+	b = u;
+	snprint(buf, sizeof buf, "%lud:%lud", a, b);
+	return fmtstrcpy(f, buf);
+}
+
+static void
+imap4cmd(Imap *imap, char *fmt, ...)
+{
+	char buf[256], *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");
+	idprint(imap, "-> %s\n", buf);
+	strcpy(p, "\r\n");
+	Bwrite(&imap->bout, buf, strlen(buf));
+	Bflush(&imap->bout);
+}
+
+enum {
+	Ok,
+	No,
+	Bad,
+	Bye,
+	Exists,
+	Status,
+	Fetch,
+	Cap,
+	Auth,
+	Expunge,
+
+	Unknown,
+};
+
+static char *verblist[] = {
+	[Ok]		"ok",
+	[No]		"no",
+	[Bad]		"bad",
+	[Bye]		"bye",
+	[Exists]	"exists",
+	[Status]	"status",
+	[Fetch]		"fetch",
+	[Cap]		"capability",
+	[Auth]		"authenticate",
+	[Expunge]	"expunge",
+};
+
+static int
+verbcode(char *verb)
+{
+	int i;
+	char *q;
+
+	if(q = strchr(verb, ' '))
+		*q = '\0';
+	for(i = 0; i < nelem(verblist); i++)
+		if(strcmp(verblist[i], verb) == 0)
+			break;
+	if(q)
+		*q = ' ';
+	return i;
+}
+
+static vlong
+mkuid(Imap *i, char *id)
+{
+	vlong v;
+
+	idprint(i, "mkuid: validity: %lud, idstr: '%s', val: %lud\n", i->validity, id, strtoul(id, 0, 10));
+	v = (vlong)i->validity<<32;
+	return v | strtoul(id, 0, 10);
+}
+
+static vlong
+xnum(char *s, int a, int b)
+{
+	vlong v;
+
+	if(*s != a)
+		return -1;
+	v = strtoull(s + 1, &s, 10);
+	if(*s != b)
+		return -1;
+	return v;
+}
+
+static struct{
+	char	*flag;
+	int	e;
+} ftab[] = {
+	"\\Answered",	Fanswered,
+	"\\Deleted",	Fdeleted,
+	"\\Draft",	Fdraft,
+	"\\Flagged",	Fflagged,
+	"\\Recent",	Frecent,
+	"\\Seen",	Fseen,
+	"\\Stored",	Fstored,
+};
+
+static int
+parseflags(char *s)
+{
+	char *f[10];
+	int i, j, n, r;
+
+	r = 0;
+	n = tokenize(s, f, nelem(f));
+	for(i = 0; i < n; i++)
+		for(j = 0; j < nelem(ftab); j++)
+			if(cistrcmp(f[i], ftab[j].flag) == 0)
+				r |= ftab[j].e;
+	return r;
+}
+
+/* "17-Jul-1996 02:44:25 -0700" */
+long
+internaltounix(char *s)
+{
+	Tm tm;
+
+	if(tmparse(&tm, "?DD-?MM-YYYY hh:mm:ss ?Z", s, nil, nil) == nil)
+		return -1;
+	return tmnorm(&tm);
+}
+	
+static char*
+qtoken(char *s, char *sep)
+{
+	int quoting;
+	char *t;
+
+	quoting = 0;
+	t = s;	/* s is output string, t is input string */
+	while(*t != '\0' && (quoting || utfrune(sep, *t) == nil)){
+		if(*t != '"' && *t  != '(' && *t != ')'){
+			*s++ = *t++;
+			continue;
+		}
+		/* *t is a quote */
+		if(!quoting || *t == '('){
+			quoting++;
+			t++;
+			continue;
+		}
+		/* quoting and we're on a quote */
+		if(t[1] != '"'){
+			/* end of quoted section; absorb closing quote */
+			t++;
+			if(quoting > 0)
+				quoting--;
+			continue;
+		}
+		/* doubled quote; fold one quote into two */
+		t++;
+		*s++ = *t++;
+	}
+	if(*s != '\0'){
+		*s = '\0';
+		if(t == s)
+			t++;
+	}
+	return t;
+}
+
+int
+imaptokenize(char *s, char **args, int maxargs)
+{
+	int nargs;
+
+	for(nargs=0; nargs < maxargs; nargs++){
+		while(*s != '\0' && utfrune(qsep, *s) != nil)
+			s++;
+		if(*s == '\0')
+			break;
+		args[nargs] = s;
+		s = qtoken(s, qsep);
+	}
+
+	return nargs;
+}
+
+static char*
+fetchrsp(Imap *imap, char *p, Mailbox *, Message *m, int idx)
+{
+	char *f[15], *s, *q;
+	int i, n, a;
+	ulong o, l;
+	uvlong v;
+	static char error[256];
+	extern void msgrealloc(Message*, ulong);
+
+	if(idx < 0 || idx >= imap->muid){
+		snprint(error, sizeof error, "fetchrsp: bad idx %d", idx);
+		return error;
+	}
+
+redux:
+	n = imaptokenize(p, f, nelem(f));
+	if(n%2)
+		return confused;
+	for(i = 0; i < n; i += 2){
+		if(strcmp(f[i], "internaldate") == 0){
+			l = internaltounix(f[i + 1]);
+			if(l < 418319360)
+				abort();
+			if(idx < imap->muid)
+				imap->f[idx].dates = l;
+		}else if(strcmp(f[i], "rfc822.size") == 0){
+			l = strtoul(f[i + 1], 0, 0);
+			if(m)
+				m->size = l;
+			else if(idx < imap->muid)
+				imap->f[idx].sizes = l;
+		}else if(strcmp(f[i], "uid") == 0){
+			v = mkuid(imap, f[i + 1]);
+			if(m)
+				m->imapuid = v;
+			if(idx < imap->muid)
+				imap->f[idx].uid = v;
+		}else if(strcmp(f[i], "flags") == 0){
+			l = parseflags(f[i + 1]);
+			if(m)
+				m->flags = l;
+			if(idx < imap->muid)
+				imap->f[idx].flags = l;
+		}else if(strncmp(f[i], "body[]", 6) == 0){
+			s = f[i]+6;
+			o = 0;
+			if(*s == '<')
+				o = xnum(s, '<', '>');
+			if(o == -1)
+				return confused;
+			l = xnum(f[i + 1], '{', '}');
+			a = o + l - m->ibadchars - m->size;
+			if(a > 0){
+				assert(imap->flags & Fgmail);
+				m->size = o + l;
+				msgrealloc(m, m->size);
+				m->size -= m->ibadchars;
+			}
+			if(Bread(&imap->bin, m->start + o, l) != l){
+				snprint(error, sizeof error, "read: %r");
+				return error;
+			}
+			if(Bgetc(&imap->bin) == ')'){
+				while(Bgetc(&imap->bin) != '\n')
+					;
+				return 0;
+			}
+			/* evil */
+			if(!(p = Brdline(&imap->bin, '\n')))
+				return 0;
+			q = p + Blinelen(&imap->bin);
+			while(q > p && (q[-1] == '\n' || q[-1] == '\r'))
+				q--;
+			*q = 0;
+			lowercase(p);
+			idprint(imap, "<- %s\n", p);
+
+			goto redux;
+		}else
+			return confused;
+	}
+	return nil;
+}
+
+void
+parsecap(Imap *imap, char *s)
+{
+	char *t[32], *p;
+	int n, i;
+
+	s = strdup(s);
+	n = getfields(s, t, nelem(t), 0, " ");
+	for(i = 0; i < n; i++){
+		if(strncmp(t[i], "auth=", 5) == 0){
+			p = t[i] + 5;
+			if(strcmp(p, "cram-md5") == 0)
+				imap->cap |= Ccram;
+			if(strcmp(p, "ntlm") == 0)
+				imap->cap |= Cntlm;
+		}else if(strcmp(t[i], "logindisabled") == 0)
+			imap->cap |= Cnolog;
+	}
+	free(s);
+}
+
+/*
+ *  get imap4 response line.  there might be various
+ *  data or other informational lines mixed in.
+ */
+static char*
+imap4resp0(Imap *imap, Mailbox *mb, Message *m)
+{
+	char *e, *line, *p, *ep, *op, *q, *verb;
+	int n, idx, unexp;
+	static char error[256];
+
+	unexp = 0;
+	while(p = Brdline(&imap->bin, '\n')){
+		ep = p + Blinelen(&imap->bin);
+		while(ep > p && (ep[-1] == '\n' || ep[-1] == '\r'))
+			*--ep = '\0';
+		idprint(imap, "<- %s\n", p);
+		if(unexp && p[0] != '9' && p[1] != 'x')
+		if(strtoul(p + 2, &p, 10) != imap->tag)
+			continue;
+		if(p[0] != '+')
+			lowercase(p);		/* botch */
+
+		switch(p[0]){
+		case '+':				/* cram challenge */
+			if(ep - p > 2)
+				return p + 2;
+			break;
+		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 Bye:
+				/* early disconnect */
+				snprint(error, sizeof error, "%s", p);
+				return error;
+			case Ok:
+			case No:
+			case Bad:
+				/* human readable text at p; */
+				break;
+			case Exists:
+				imap->nmsg = n;
+				break;
+			case Cap:
+				parsecap(imap, p);
+				break;
+			case Status:
+				/* * status inbox (messages 2 uidvalidity 960164964) */
+				if(q = strstr(p, "messages"))
+					imap->nmsg = strtoul(q + 8, 0, 10);
+				if(q = strstr(p, "uidvalidity"))
+					imap->newvalidity = strtoul(q + 11, 0, 10);
+				break;
+			case Fetch:
+				if(*p == '('){
+					p++;
+					if(ep[-1] == ')')
+						*--ep = 0;
+				}
+				if(e = fetchrsp(imap, p, mb, m, n - 1))
+					eprint("imap: fetchrsp: %s\n", e);
+				if(n > 0 && n <= imap->muid && n > imap->nuid)
+					imap->nuid = n;
+				break;
+			case Expunge:
+				if(n < 1 || n > imap->muid || (n - 1) >= imap->nmsg){
+					snprint(error, sizeof(error), "bad expunge %d (nmsg %d)", n, imap->nuid);
+					return error;
+				}
+				idx = n - 1;
+				memmove(&imap->f[idx], &imap->f[idx + 1], (imap->nmsg - idx - 1)*sizeof(imap->f[0]));
+				imap->nmsg--;
+				imap->nuid--;
+				break;
+			case Auth:
+				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;
+			}
+			eprint("imap: expected %lud; got %s\n", imap->tag, op);
+			break;
+		default:
+			if(imap->flags&Fdebug || *p){
+				eprint("imap: unexpected line: %s\n", p);
+				unexp = 1;
+			}
+		}
+	}
+	snprint(error, sizeof error, "i/o error: %r\n");
+	return error;
+}
+
+static char*
+imap4resp(Imap *i)
+{
+	return imap4resp0(i, 0, 0);
+}
+
+static int
+isokay(char *resp)
+{
+	return cistrncmp(resp, "OK", 2) == 0;
+}
+
+static char*
+findflag(int idx)
+{
+	int i;
+
+	for(i = 0; i < nelem(ftab); i++)
+		if(ftab[i].e == 1<<idx)
+			return ftab[i].flag;
+	return nil;
+}
+
+static void
+imap4modflags(Mailbox *mb, Message *m, int flags)
+{
+	char buf[128], *p, *e, *fs;
+	int i, f;
+	Imap *imap;
+
+	imap = mb->aux;
+	e = buf + sizeof buf;
+	p = buf;
+	f = flags & ~Frecent;
+	for(i = 0; i < Nflags; i++)
+		if(f & 1<<i && (fs = findflag(i)))
+			p = seprint(p, e, "%s ", fs);
+	if(p > buf){
+		p[-1] = 0;
+		imap4cmd(imap, "uid store %lud flags (%s)", (ulong)m->imapuid, buf);
+		imap4resp0(imap, mb, m);
+	}
+}
+
+static char*
+imap4cram(Imap *imap)
+{
+	char *s, *p, ch[128], usr[64], rbuf[128], ubuf[128], ebuf[192];
+	int i, n, l;
+
+	fmtinstall('[', encodefmt);
+
+	imap4cmd(imap, "authenticate cram-md5");
+	p = imap4resp(imap);
+	if(p == nil)
+		return "no challenge";
+	l = dec64((uchar*)ch, sizeof ch, p, strlen(p));
+	if(l == -1)
+		return "bad base64";
+	ch[l] = 0;
+	idprint(imap, "challenge [%s]\n", ch);
+
+	if(imap->user == nil)
+		imap->user = getlog();
+	n = auth_respond(ch, l, usr, sizeof usr, rbuf, sizeof rbuf, auth_getkey,
+		"proto=cram role=client server=%q user=%s", imap->host, imap->user);
+	if(n == -1)
+		return "cannot find IMAP password";
+	for(i = 0; i < n; i++)
+		rbuf[i] = tolower(rbuf[i]);
+	l = snprint(ubuf, sizeof ubuf, "%s %.*s", usr, utfnlen(rbuf, n), rbuf);
+	idprint(imap, "raw cram [%s]\n", ubuf);
+	snprint(ebuf, sizeof ebuf, "%.*[", l, ubuf);
+
+	imap->tag = 1;
+	idprint(imap, "-> %s\n", ebuf);
+	Bprint(&imap->bout, "%s\r\n", ebuf);
+	Bflush(&imap->bout);
+
+	if(!isokay(s = imap4resp(imap)))
+		return s;
+	return nil;
+}
+
+/*
+ *  authenticate to IMAP4 server using NTLM (untested)
+ * 
+ *  http://davenport.sourceforge.net/ntlm.html#ntlmImapAuthentication
+ *  http://msdn.microsoft.com/en-us/library/cc236621%28PROT.13%29.aspx
+ */
+static uchar*
+psecb(uchar *p, uint o, int n)
+{
+	p[0] = n;
+	p[1] = n>>8;
+	p[2] = n;
+	p[3] = n>>8;
+	p[4] = o;
+	p[5] = o>>8;
+	p[6] = o>>16;
+	p[7] = o>>24;
+	return p+8;
+}
+
+static uchar*
+psecq(uchar *q, char *s, int n)
+{
+	memcpy(q, s, n);
+	return q+n;
+}
+
+static char*
+imap4ntlm(Imap *imap)
+{
+	char *s, ruser[64], enc[256];
+	uchar buf[128], *p, *ep, *q, *eq, *chal;
+	int n;
+	MSchapreply mcr;
+
+	imap4cmd(imap, "authenticate ntlm");
+	imap4resp(imap);
+
+	/* simple NtLmNegotiate blob with NTLM+OEM flags */
+	imap4cmd(imap, "TlRMTVNTUAABAAAAAgIAAA==");
+	s = imap4resp(imap);
+	n = dec64(buf, sizeof buf, s, strlen(s));
+	if(n < 32 || memcmp(buf, "NTLMSSP", 8) != 0)
+		return "bad NtLmChallenge";
+	chal = buf+24;
+
+	if(auth_respond(chal, 8, ruser, sizeof ruser,
+			&mcr, sizeof mcr, auth_getkey,
+			"proto=mschap role=client service=imap server=%q user?",
+			imap->host) < 0)
+		return "auth_respond failed";
+
+	/* prepare NtLmAuthenticate blob */
+	memset(buf, 0, sizeof(buf));
+	p = buf;
+	ep = p + 8 + 6*8 + 2*4;
+	q = ep;
+	eq = buf + sizeof buf;
+
+
+	memcpy(p, "NTLMSSP", 8);	/* magic */
+	p += 8;
+
+	*p++ = 3;
+	*p++ = 0;
+	*p++ = 0;
+	*p++ = 0;
+
+	p = psecb(p, q-buf, 24);		/* LMresp */
+	q = psecq(q, mcr.LMresp, 24);
+
+	p = psecb(p, q-buf, 24);		/* NTresp */
+	q = psecq(q, mcr.NTresp, 24);
+
+	p = psecb(p, q-buf, 0);		/* realm */
+
+	n = strlen(ruser);
+	p = psecb(p, q-buf, n);		/* user name */
+	q = psecq(q, ruser, n);
+
+	p = psecb(p, q-buf, 0);		/* workstation name */
+	p = psecb(p, q-buf, 0);		/* session key */
+
+	*p++ = 0x02;			/* flags: oem(2)|ntlm(0x200) */
+	*p++ = 0x02;
+	*p++ = 0;
+	*p++ = 0;
+
+	if(p > ep || q > eq)
+		return "error creating NtLmAuthenticate";
+	enc64(enc, sizeof enc, buf, q-buf);
+
+	imap4cmd(imap, enc);
+	if(!isokay(s = imap4resp(imap)))
+		return s;
+	return nil;
+}
+
+static char*
+imap4passwd(Imap *imap)
+{
+	char *s;
+	UserPasswd *up;
+
+	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;
+	return nil;
+}
+
+static char*
+imap4login(Imap *imap)
+{
+	char *e;
+
+	if(imap->cap & Ccram)
+		e = imap4cram(imap);
+	else if(imap->cap & Cntlm)
+		e = imap4ntlm(imap);
+	else
+		e = imap4passwd(imap);
+	if(e)
+		return e;
+	imap4cmd(imap, "select %Z", imap->mbox);
+	if(!isokay(e = imap4resp(imap)))
+		return e;
+	return nil;
+}
+
+static char*
+imaperrstr(char *host, char *port)
+{
+	char err[ERRMAX];
+	static char buf[256];
+
+	err[0] = 0;
+	errstr(err, sizeof err);
+	snprint(buf, sizeof buf, "%s/%s:%s", host, port, err);
+	return buf;
+}
+
+static void
+imap4disconnect(Imap *imap)
+{
+	if(imap->binit){
+		Bterm(&imap->bin);
+		Bterm(&imap->bout);
+		imap->binit = 0;
+	}
+	if(imap->fd >= 0){
+		close(imap->fd);
+		imap->fd = -1;
+	}
+}
+
+char*
+capabilties(Imap *imap)
+{
+	char * err;
+
+	imap4cmd(imap, "capability");
+	imap4resp(imap);
+	err = imap4resp(imap);
+	if(isokay(err))
+		err = 0;
+	return err;
+}
+
+static char*
+imap4dial(Imap *imap)
+{
+	char *err, *port;
+
+	if(imap->fd >= 0){
+		imap4cmd(imap, "noop");
+		if(isokay(imap4resp(imap)))
+			return nil;
+		imap4disconnect(imap);
+	}
+	if(imap->flags & Fssl)
+		port = "imaps";
+	else
+		port = "imap";
+	if((imap->fd = dial(netmkaddr(imap->host, "net", port), 0, 0, 0)) < 0)
+		return imaperrstr(imap->host, port);
+	if(imap->flags & Fssl && (imap->fd = wraptls(imap->fd, imap->host)) < 0){
+		err = imaperrstr(imap->host, port);
+		imap4disconnect(imap);
+		return err;
+	}
+	assert(imap->binit == 0);
+	Binit(&imap->bin, imap->fd, OREAD);
+	Binit(&imap->bout, imap->fd, OWRITE);
+	imap->binit = 1;
+
+	imap->tag = 0;
+	err = imap4resp(imap);
+	if(!isokay(err))
+		return "error in initial IMAP handshake";
+
+	if((err = capabilties(imap)) || (err = imap4login(imap))){
+		eprint("imap: err is %s\n", err);
+		imap4disconnect(imap);
+		return err;
+	}
+	return nil;
+}
+
+/* gmail lies about message sizes */
+static ulong
+gmaildiscount(Message *m, uvlong o, ulong l)
+{
+	if((m->cstate&Cidx) == 0)
+	if(o + l == m->size)
+		return l + 100 + (o + l)/5;
+	return l;
+}
+
+static int
+imap4fetch(Mailbox *mb, Message *m, uvlong o, ulong l)
+{
+	Imap *imap;
+	char *resp;
+
+	imap = mb->aux;
+	if(imap->flags & Fgmail)
+		l = gmaildiscount(m, o, l);
+	idprint(imap, "uid fetch %lud (flags body.peek[]<%llud.%lud>)\n", (ulong)m->imapuid, o, l);
+	imap4cmd(imap, "uid fetch %lud (flags body.peek[]<%llud.%lud>)", (ulong)m->imapuid, o, l);
+	resp = imap4resp0(imap, mb, m);
+	if(!isokay(resp)){
+		eprint("imap: imap fetch failed: %s\n", resp);
+		return -1;
+	}
+	return 0;
+}
+
+static uvlong
+datesec(Imap *imap, int i)
+{
+	int j;
+	uvlong v;
+	Fetchi *f;
+
+	f = imap->f;
+	v = (uvlong)f[i].dates << 8;
+
+	/* shifty; these sequences should be stable. */
+	for(j = i; j-- > 0; )
+		if(f[i].dates != f[j].dates)
+			break;
+	v |= i - (j + 1);
+	return v;
+}
+
+static int
+vcmp(vlong a, vlong b)
+{
+	a -= b;
+	if(a > 0)
+		return 1;
+	if(a < 0)
+		return -1;
+	return 0;
+}
+
+static int
+fetchicmp(Fetchi *f1, Fetchi *f2)
+{
+	return vcmp(f1->uid, f2->uid);
+}
+
+static char*
+imap4read(Imap *imap, Mailbox *mb)
+{
+	char *s;
+	int i, n, c;
+	Fetchi *f;
+	Message *m, **ll;
+
+again:
+	imap4cmd(imap, "status %Z (messages uidvalidity)", imap->mbox);
+	if(!isokay(s = imap4resp(imap)))
+		return s;
+	/* the world shifted: start over */
+	if(imap->validity != imap->newvalidity){
+		imap->validity = imap->newvalidity;
+		imap->nuid = 0;
+		imap->muid = 0;
+		imap->nmsg = 0;
+		goto again;
+	}
+
+	imap->f = erealloc(imap->f, imap->nmsg*sizeof imap->f[0]);
+	if(imap->nmsg > imap->muid)
+		memset(&imap->f[imap->muid], 0, (imap->nmsg - imap->muid)*sizeof(imap->f[0]));
+	imap->muid = imap->nmsg;
+	if(imap->nmsg > 0){
+		n = imap->nuid;
+		if(n == 0)
+			n = 1;
+		if(n > imap->nmsg)
+			n = imap->nmsg;
+		imap4cmd(imap, "fetch %d:%d (uid flags rfc822.size internaldate)", n, imap->nmsg);
+		if(!isokay(s = imap4resp(imap)))
+			return s;
+	}
+
+	f = imap->f;
+	n = imap->nuid;
+	if(n > imap->muid){
+		idprint(imap, "partial sync %d > %d\n", n, imap->muid);
+		n = imap->nuid = imap->muid;
+	} else if(n < imap->nmsg)
+		idprint(imap, "partial sync %d < %d\n", n, imap->nmsg);
+	qsort(f, n, sizeof f[0], (int(*)(void*, void*))fetchicmp);
+	ll = &mb->root->part;
+	for(i = 0; (m = *ll) != nil || i < n; ){
+		c = -1;
+		if(i >= n)
+			c = 1;
+		else if(m){
+			if(m->imapuid == 0)
+				m->imapuid = strtoull(m->idxaux, 0, 0);
+			c = vcmp(f[i].uid, m->imapuid);
+		}
+		if(c < 0){
+			/* new message */
+			idprint(imap, "new: %U (%U)\n", f[i].uid, m ? m->imapuid: 0);
+			if(f[i].sizes == 0 || f[i].sizes > Maxmsg){
+				idprint(imap, "skipping bad size: %lud\n", f[i].sizes);
+				i++;
+				continue;
+			}
+			m = newmessage(mb->root);
+			m->inmbox = 1;
+			m->idxaux = smprint("%llud", f[i].uid);
+			m->imapuid = f[i].uid;
+			m->fileid = datesec(imap, i);
+			m->size = f[i].sizes;
+			m->flags = f[i].flags;
+			m->next = *ll;
+			*ll = m;
+			ll = &m->next;
+			i++;
+		}else if(c > 0){
+			/* deleted message; */
+			idprint(imap, "deleted: %U (%U)\n", i<n? f[i].uid: 0, m? m->imapuid: 0);
+			m->inmbox = 0;
+			m->deleted = Disappear;
+			ll = &m->next;
+		}else{
+			if((m->flags & ~Frecent) != (f[i].flags & ~Frecent)){
+				idprint(imap, "modified: %d != %d\n", m->flags, f[i].flags);
+				m->cstate |= Cmod;
+			}
+			m->flags = f[i].flags;
+			ll = &m->next;
+			i++;
+		}
+	}
+	return nil;
+}
+
+static void
+imap4delete(Mailbox *mb, Message *m)
+{
+	Imap *imap;
+
+	imap = mb->aux;
+	if((ulong)(m->imapuid>>32) == imap->validity){
+		imap4cmd(imap, "uid store %lud +flags (\\Deleted)", (ulong)m->imapuid);
+		imap4resp(imap);
+		imap4cmd(imap, "expunge");
+		imap4resp(imap);
+//		if(!isokay(imap4resp(imap))
+//			return -1;
+	}
+	m->inmbox = 0;
+}
+static char*
+imap4move(Mailbox *mb, Message *m, char *dest)
+{
+	char *r;
+	Imap *imap;
+
+	imap = mb->aux;
+	imap4cmd(imap, "uid copy %lud %s", (ulong)m->imapuid, dest);
+	r = imap4resp(imap);
+	if(!isokay(r))
+		return r;
+	imap4cmd(imap, "uid store %lud +flags (\\Deleted)", (ulong)m->imapuid);
+	r = imap4resp(imap);
+	if(!isokay(r))
+		return r;
+	imap4cmd(imap, "expunge");
+	r = imap4resp(imap);
+	if(!isokay(r))
+		return r;
+	m->inmbox = 0;
+	return 0;
+}
+
+static char*
+imap4sync(Mailbox *mb)
+{
+	char *err;
+	Imap *imap;
+
+	imap = mb->aux;
+	if(err = imap4dial(imap))
+		goto out;
+	err = imap4read(imap, mb);
+out:
+	mb->waketime = (ulong)time(0) + imap->refreshtime;
+	return err;
+}
+
+static char*
+imap4ctl(Mailbox *mb, int argc, char **argv)
+{
+	Imap *imap;
+
+	imap = mb->aux;
+	if(argc < 1)
+		return Eimap4ctl;
+
+	if(argc == 1 && strcmp(argv[0], "debug") == 0){
+		imap->flags ^= Fdebug;
+		return nil;
+	}
+	if(argc == 2 && strcmp(argv[0], "uid") == 0){
+		uvlong l;
+		Message *m;
+
+		for(m = mb->root->part; m; m = m->next)
+			if(strcmp(argv[1], m->name) == 0){
+				l = strtoull(m->idxaux, 0, 0);
+				fprint(2, "uid %s %lud %lud %lud %lud\n", m->name, (ulong)(l>>32), (ulong)l,
+					(ulong)(m->imapuid>>32), (ulong)m->imapuid);
+			}
+		return nil;
+	}
+	if(strcmp(argv[0], "refresh") == 0)
+		switch(argc){
+		case 1:
+			imap->refreshtime = 60;
+			return nil;
+		case 2:
+			imap->refreshtime = atoi(argv[1]);
+			return nil;
+		}
+
+	return Eimap4ctl;
+}
+
+static void
+imap4close(Mailbox *mb)
+{
+	Imap *imap;
+
+	imap = mb->aux;
+	imap4disconnect(imap);
+	free(imap->f);
+	free(imap);
+}
+
+static char*
+mkmbox(Imap *imap, char *p, char *e)
+{
+	p = seprint(p, e, "%s/box/%s/imap.%s", MAILROOT, getlog(), imap->host);
+	if(imap->user && strcmp(imap->user, getlog()))
+		p = seprint(p, e, ".%s", imap->user);
+	if(cistrcmp(imap->mbox, "inbox"))
+		p = seprint(p, e, ".%s", imap->mbox);
+	return p;
+}
+
+static char*
+findmbox(char *p)
+{
+	char *f[10], path[Pathlen];
+	int nf;
+
+	snprint(path, sizeof path, "%s", p);
+	nf = getfields(path, f, 5, 0, "/");
+	if(nf < 3)
+		return nil;
+	return f[nf - 1];
+}
+
+static char*
+imap4rename(Mailbox *mb, char *p2, int)
+{
+	char *r, *new;
+	Imap *imap;
+
+	imap = mb->aux;
+	new = findmbox(p2);
+	idprint(imap, "rename %s %s\n", imap->mbox, new);
+	imap4cmd(imap, "rename %s %s", imap->mbox, new);
+	r = imap4resp(imap);
+	if(!isokay(r))
+		return r;
+	free(imap->mbox);
+	imap->mbox = smprint("%s", new);
+	mkmbox(imap, mb->path, mb->path + sizeof mb->path);
+	return 0;
+}
+
+char*
+imap4mbox(Mailbox *mb, char *path)
+{
+	char *f[10];
+	uchar flags;
+	int nf;
+	Imap *imap;
+
+	fmtinstall('Z', Zfmt);
+	fmtinstall('U', Ufmt);
+	if(strncmp(path, "/imap/", 6) == 0)
+		flags = 0;
+	else if(strncmp(path, "/imaps/", 7) == 0)
+		flags = Fssl;
+	else
+		return Enotme;
+
+	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->freep = path;
+	imap->flags = flags;
+	imap->host = f[2];
+	if(strstr(imap->host, "gmail.com"))
+		imap->flags |= Fgmail;
+	imap->refreshtime = 60;
+	if(nf < 4)
+		imap->user = nil;
+	else
+		imap->user = f[3];
+	if(nf < 5)
+		imap->mbox = strdup("inbox");
+	else
+		imap->mbox = strdup(f[4]);
+	mkmbox(imap, mb->path, mb->path + sizeof mb->path);
+	mb->aux = imap;
+	mb->sync = imap4sync;
+	mb->close = imap4close;
+	mb->ctl = imap4ctl;
+	mb->fetch = imap4fetch;
+	mb->delete = imap4delete;
+	mb->rename = imap4rename;
+	mb->modflags = imap4modflags;
+	mb->move = imap4move;
+	mb->addfrom = 1;
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/mbox.c
@@ -1,0 +1,1725 @@
+#include "common.h"
+#include <ctype.h>
+#include <plumb.h>
+#include <libsec.h>
+#include "dat.h"
+
+typedef struct Header Header;
+struct Header {
+	char	*type;
+	uintptr	offset;
+	char	*(*f)(Message*, Header*, char*, char*);
+	int	len;
+	int	str;
+};
+
+/* headers */
+static	char	*ctype(Message*, Header*, char*, char*);
+static	char	*cencoding(Message*, Header*, char*, char*);
+static	char	*cdisposition(Message*, Header*, char*, char*);
+static	char	*from822(Message*, Header*, char*, char*);
+static	char	*replace822(Message*, Header*, char*, char*);
+static	char	*concat822(Message*, Header*, char*, char*);
+static	char	*copy822(Message*, Header*, char*, char*);
+static	char	*ref822(Message*, Header*, char*, char*);
+
+enum
+{
+	Mhead	= 11,	/* offset of first mime header */
+};
+
+#define O(x)	offsetof(Message, x)
+static Header head[] =
+{
+	"date:",		O(date822),	copy822,		0,	0,
+	"from:", 		O(from),		from822,	0,	1,
+	"to:", 		O(to),		concat822,	0,	1,
+	"sender:",	O(sender),	replace822,	0,	1,
+	"reply-to:",	O(replyto),	replace822,	0,	1,
+	"subject:",	O(subject),	copy822,		0,	1,
+	"cc:", 		O(cc),		concat822,	0,	1,
+	"bcc:",		O(bcc),		concat822,	0,	1,
+	"in-reply-to:",	O(inreplyto),	replace822,	0,	1,
+	"message-id:",	O(messageid),	replace822,	0,	1,
+	"references:",	~0,		ref822,		0,	0,
+
+[Mhead]	"content-type:", 	~0,		ctype,		0, 	0,
+	"content-transfer-encoding:", ~0,	cencoding,	0,	0,
+	"content-disposition:", ~0,		cdisposition,	0,	0,
+};
+
+static Mailboxinit *boxinit[] = {
+	imap4mbox,
+	pop3mbox,
+	mdirmbox,
+	plan9mbox,
+};
+
+static void delmessage(Mailbox*, Message*);
+static void mailplumb(Mailbox*, Message*);
+
+char*
+syncmbox(Mailbox *mb, int doplumb)
+{
+	char *s;
+	int n, d, y, a;
+	Message *m, *next;
+
+	if(mb->syncing)
+		return nil;
+
+	mb->syncing = 1;
+
+	a = mb->root->subname;
+	if(rdidxfile(mb) == -2)
+		wridxfile(mb);
+	if(s = mb->sync(mb)){
+		mb->syncing = 0;
+		return s;
+	}
+	n = 0;
+	d = 0;
+	y = 0;
+	for(m = mb->root->part; m; m = next){
+		next = m->next;
+		if(m->deleted == 0){
+			if((m->cstate & Cidx) == 0){
+				cachehash(mb, m);
+				m->cstate |= Cnew;
+				n++;
+			}
+			if((doplumb && m->cstate & (Cnew|Cmod)) && ensurecache(mb, m) == 0){
+				mailplumb(mb, m);
+				msgdecref(mb, m);
+			}
+			m->cstate &= ~(Cnew|Cmod);
+		}
+		if(m->cstate & Cidxstale)
+			y++;
+		if(m->deleted == 0 || m->refs > 0)
+			continue;
+		if(mb->delete && m->inmbox && m->deleted & Deleted)
+			mb->delete(mb, m);
+		if(!m->inmbox){
+			if(doplumb)
+				mailplumb(mb, m);
+			delmessage(mb, m);
+			d++;
+		}
+	}
+	a = mb->root->subname - a;
+	assert(a >= 0);
+	if(n + d + y + a){
+		Hash *h;
+
+		iprint("deleted: %d; new %d; stale %d\n", d, n, y);
+		logmsg(nil, "deleted: %d; new %d; stale %d", d, n, y);
+		wridxfile(mb);
+
+		mb->vers++;
+		if(mb->refs > 0 && (h = hlook(PATH(0, Qtop), mb->name)) != nil && h->mb == mb)
+			h->qid.vers = mb->vers;
+	}
+
+	mb->syncing = 0;
+
+	return nil;
+}
+
+/*
+ * not entirely clear where the locking should take place, if
+ * it is required.
+ */
+char*
+mboxrename(char *a, char *b, int flags)
+{
+	char f0[Pathlen + 4], f1[Pathlen + 4], *err, *p0, *p1;
+	Mailbox *mb;
+
+	snprint(f0, sizeof f0, "%s", a);
+	snprint(f1, sizeof f1, "%s", b);
+	err = newmbox(f0, nil, 0, &mb);
+	dprint("mboxrename %s %s -> %s\n", f0, f1, err);
+	if(err == nil && mb->rename == nil)
+		err = "rename not supported";
+	if(err)
+		goto done;
+	err = mb->rename(mb, f1, flags);
+	if(err)
+		goto done;
+	if(flags & Rtrunc)
+		/* we're comitted, so forget bailing */
+		err = newmbox(f0, nil, DMcreate, 0);
+	p0 = f0 + strlen(f0);
+	p1 = f1 + strlen(f1);
+
+	strcat(f0, ".idx");
+	strcat(f1, ".idx");
+	rename(f0, f1, 0);
+
+	*p0 = *p1 = 0;
+	strcat(f0, ".imp");
+	strcat(f1, ".imp");
+	rename(f0, f1, 0);
+
+	hfree(PATH(0, Qtop), mb->name);
+	snprint(mb->path, sizeof mb->path, "%s", b);
+	p0 = strrchr(mb->path, '/') + 1;
+	if(p0 == (char*)1)
+		p0 = mb->path;
+	snprint(mb->name, sizeof mb->name, "%s", p0);
+	henter(PATH(0, Qtop), mb->name,
+		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+done:
+	return err;
+}
+
+static void
+initheaders(void)
+{
+	int i;
+	static int already;
+
+	if(already)
+		return;
+	already = 1;
+	for(i = 0; i < nelem(head); i++)
+		head[i].len = strlen(head[i].type);
+}
+
+static ulong
+newid(void)
+{
+	ulong rv;
+	static ulong id;
+	static Lock idlock;
+
+	lock(&idlock);
+	rv = ++id;
+	unlock(&idlock);
+
+	return rv;
+}
+
+char*
+newmbox(char *path, char *name, int flags, Mailbox **r)
+{
+	char *p, *rv;
+	int i;
+	Mailbox *mb, **l;
+
+	if(r)
+		*r = nil;
+	initheaders();
+	mb = emalloc(sizeof *mb);
+	mb->flags = flags;
+	strncpy(mb->path, path, sizeof mb->path - 1);
+	p = name;
+	if(p == 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);
+	mb->idxread = genericidxread;
+	mb->idxwrite = genericidxwrite;
+	mb->idxinvalid = genericidxinvalid;
+
+	/* check for a mailbox type */
+	rv = Enotme;	/* can't happen; shut compiler up */
+	for(i = 0; i < nelem(boxinit); i++)
+		if((rv = boxinit[i](mb, path)) != Enotme)
+			break;
+	if(rv){
+		free(mb);
+		return rv;
+	}
+
+	/* make sure name isn't taken */
+	for(l = &mbl; *l != nil; l = &(*l)->next)
+		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);
+			return rv;
+		}
+
+	/* always try locking */
+	mb->dolock = 1;
+	mb->refs = 1;
+	mb->next = nil;
+	mb->id = newid();
+	mb->root = newmessage(nil);
+	mtreeinit(mb);
+
+	*l = mb;
+
+	henter(PATH(0, Qtop), mb->name,
+		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+	if(mb->ctl)
+		henter(PATH(mb->id, Qmbox), "ctl",
+			(Qid){PATH(mb->id, Qmboxctl), 0, QTFILE}, nil, mb);
+	if(r)
+		*r = mb;
+
+	return syncmbox(mb, 0);
+}
+
+/* close the named mailbox */
+void
+freembox(char *name)
+{
+	Mailbox **l, *mb;
+
+	for(l = &mbl; (mb = *l) != nil; l = &mb->next)
+		if(strcmp(name, mb->name) == 0){
+			*l = mb->next;
+			mb->next = nil;
+			hfree(PATH(0, Qtop), mb->name);
+			if(mb->ctl)
+				hfree(PATH(mb->id, Qmbox), "ctl");
+			mboxdecref(mb);
+			break;
+		}
+}
+
+char*
+removembox(char *name, int flags)
+{
+	Mailbox *mb;
+
+	for(mb = mbl; mb != nil; mb = mb->next)
+		if(strcmp(name, mb->path) == 0){
+			mb->flags |= ORCLOSE;
+			mb->rmflags = flags;
+			freembox(mb->name);
+			return 0;
+		}
+	return "maibox not found";
+}
+
+void
+syncallmboxes(void)
+{
+	Mailbox *mb;
+	char *err;
+
+	for(mb = mbl; mb != nil; mb = mb->next)
+		if(err = syncmbox(mb, 0))
+			eprint("syncmbox: %s\n", err);
+}
+
+/*
+ *  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.
+ */
+static int
+rxtotm(Message *m, Tm *tm)
+{
+	char *p, *q;
+	int r;
+
+	if(cistrncmp(m->header, "received:", 9))
+		return -1;
+	q = strchr(m->header, ';');
+	if(!q)
+		return -1;
+	p = q;
+	while((p = strchr(p, '\n')) != nil){
+		if(p[1] != ' ' && p[1] != '\t' && p[1] != '\n')
+			break;
+		p++;
+	}
+	if(!p)
+		return -1;
+	*p = '\0';
+	r = strtotm(q + 1, tm);
+	*p = '\n';
+	return r;
+}
+
+static Message*
+gettopmsg(Mailbox *mb, Message *m)
+{
+	while(!Topmsg(mb, m))
+		m = m->whole;
+	return m;
+}
+
+static void
+datesec(Mailbox *mb, Message *m)
+{
+	vlong v;
+	Tm tm;
+
+	if(m->fileid > 1000000ull<<8)
+		return;
+	if(m->unixdate && strtotm(m->unixdate, &tm) >= 0)
+		v = tmnorm(&tm);
+	else if(m->date822 && strtotm(m->date822, &tm) >= 0)
+		v = tmnorm(&tm);
+	else if(rxtotm(m, &tm) >= 0)
+		v = tmnorm(&tm);
+	else{
+		logmsg(gettopmsg(mb, m), "%s:%s: datasec %s %s\n", mb->path,
+			m->whole? m->whole->name: "?",
+			m->name, m->type);
+		if(Topmsg(mb, m) || strcmp(m->type, "message/rfc822") == 0)
+			abort();
+		v = 0;
+	}
+	m->fileid = v<<8;
+}
+
+/*
+ *  parse a message
+ */
+extern void sanemsg(Message*);
+extern void sanembmsg(Mailbox*, Message*);
+
+static Message*
+haschild(Message *m, int i)
+{
+	for(m = m->part; m && i; i--)
+		m = m->next;
+	return m;
+}
+
+static void
+parseattachments(Message *m, Mailbox *mb)
+{
+	char *p, *x;
+	int i;
+	Message *nm, **l;
+
+	/* if there's a boundary, recurse... */
+	dprint("parseattachments %p %ld boundary %s\n", m->start, (ulong)(m->end - m->start), m->boundary);
+	if(m->boundary != nil){
+		p = m->body;
+		nm = nil;
+		l = &m->part;
+		for(i = 0;;){
+			x = strstr(p, m->boundary);
+			/* two sequential boundaries; ignore nil message */
+			if(nm && x == p){
+				p = strchr(x, '\n');
+				if(p == nil){
+					nm->rbend = nm->bend = nm->end = x;
+					sanemsg(nm);
+					break;
+				}
+				p = p + 1;
+				continue;
+			}
+			/* 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(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;
+			if((nm = haschild(m, i++)) == nil){
+				nm = newmessage(m);
+				*l = nm;
+				l = &nm->next;
+			}
+			nm->start = ++p;
+			assert(nm->ballocd == 0);
+			nm->mheader = nm->header = nm->body = nm->rbody = nm->start;
+		}
+		for(nm = m->part; nm != nil; nm = nm->next){
+			nm->size = nm->end - nm->start;
+			parse(mb, nm, 0, 1);
+			cachehash(mb, nm);		/* botchy place for this */
+		}
+		return;
+	}
+
+	/* if we've got an rfc822 message, recurse... */
+	if(strcmp(m->type, "message/rfc822") == 0){
+		if((nm = haschild(m, 0)) == nil){
+			nm = newmessage(m);
+			m->part = nm;
+		}
+		assert(nm->ballocd == 0);
+		nm->start = nm->header = nm->body = nm->rbody = m->body;
+		nm->end = nm->bend = nm->rbend = m->bend;
+		nm->size = nm->end - nm->start;
+		parse(mb, nm, 0, 0);
+		cachehash(mb, nm);			/* botchy place for this */
+	}
+}
+
+static void
+parseunix(Message *m)
+{
+	char *s, *p;
+
+	m->unixheader = smprint("%.*s", utfnlen(m->start, m->header - m->start), m->start);
+	s = m->unixheader + 5;
+	if((p = strchr(s, ' ')) == nil)
+		return;
+	*p = 0;
+	free(m->unixfrom);
+	m->unixfrom = strdup(s);
+	*p = ' ';
+	m->unixdate = ++p;
+}
+
+void
+parseheaders(Mailbox *mb, Message *m, int addfrom, int justmime)
+{
+	char *p, *e, *o, *t, *s;
+	int i, i0, n;
+	uintptr a;
+
+	if(m->header == nil)
+		m->header = m->start;
+
+	/* parse unix header */
+	if(!justmime && !addfrom && m->unixheader == nil){
+		if(strncmp(m->start, "From ", 5) == 0)
+		if((e = memchr(m->start, '\n', m->end - m->start)) != nil){
+			m->header = e + 1;
+			parseunix(m);
+		}
+	}
+
+	/* parse mime headers */
+	p = m->mheader = m->mhend = m->header;
+	i0 = 0;
+	if(justmime)
+		i0 = Mhead;
+	s = emalloc(2048);
+	e = s + 2048 - 1;
+	while((n = hdrlen(p, m->end)) > 0){
+		if(n > e - s){
+			s = erealloc(s, n);
+			e = s + n - 1;
+		}
+		rfc2047(s, e, p, n, 1);
+		p += n;
+
+		for(i = i0; i < nelem(head); i++)
+			if(!cistrncmp(s, head[i].type, head[i].len)){
+				a = head[i].offset;
+				if(a != ~0){
+					if(o = *(char**)((char*)m + a))
+						continue;
+					t = head[i].f(m, head + i, o, s);
+					*(char**)((char*)m + a) = t;
+				}else
+					head[i].f(m, head + i, 0, s);
+				break;
+			}
+	}
+	free(s);
+	/* 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;
+		m->mhend = m->header;
+	}
+	if(*p == '\n')
+		p++;
+	m->rbody = m->body = p;
+
+	if(!justmime)
+		datesec(mb, m);
+
+	/*
+	 *  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 == nil) {
+		free(m->unixheader);
+		m->unixheader = nil;
+	} else if(m->unixheader == nil){
+		if(m->unixfrom != nil && strcmp(m->unixfrom, "???") != 0)
+			p = m->unixfrom;
+		else if(m->from != nil)
+			p = m->from;
+		else
+			p = "???";
+		m->unixheader = smprint("From %s %Δ\n", p, m->fileid);
+	}
+	m->unixdate = nil;
+
+	m->cstate |= Cheader;
+sanembmsg(mb, m);
+}
+
+char*
+promote(char *s)
+{
+	return s? strdup(s): nil;
+}
+
+void
+parsebody(Message *m, Mailbox *mb)
+{
+	Message *nm;
+
+	/* recurse */
+	if(strncmp(m->type, "multipart/", 10) == 0)
+		parseattachments(m, mb);
+	else if(strcmp(m->type, "message/rfc822") == 0){
+		decode(m);
+		parseattachments(m, mb);
+		nm = m->part;
+
+		/* promote headers */
+		if(m->replyto == nil && m->from == nil && m->sender == nil){
+			m->from = promote(nm->from);
+			m->to = promote(nm->to);
+			m->date822 = promote(nm->date822);
+			m->sender = promote(nm->sender);
+			m->replyto = promote(nm->replyto);
+			m->subject = promote(nm->subject);
+		}
+	}else if(strncmp(m->type, "text/", 5) == 0)
+		sanemsg(m);
+
+	free(m->boundary);
+	m->boundary = nil;
+
+	if(m->replyto == nil){
+		if(m->from != nil)
+			m->replyto = strdup(m->from);
+		else if(m->sender != nil)
+			m->replyto = strdup(m->sender);
+		else if(m->unixfrom != nil)
+			m->replyto = strdup(m->unixfrom);
+	}
+	if(m->from == nil && m->unixfrom != nil)
+		m->from = strdup(m->unixfrom);
+
+	free(m->unixfrom);
+	m->unixfrom = nil;
+
+	m->rawbsize = m->rbend - m->rbody;
+	m->cstate |= Cbody;
+}
+
+void
+parse(Mailbox *mb, Message *m, int addfrom, int justmime)
+{
+	sanemsg(m);
+	if((m->cstate & Cheader) == 0)
+		parseheaders(mb, m, addfrom, justmime);
+	parsebody(m, mb);
+	sanemsg(m);
+}
+
+static char*
+skipwhite(char *p)
+{
+	while(isascii(*p) && isspace(*p))
+		p++;
+	return p;
+}
+
+static char*
+skiptosemi(char *p)
+{
+	while(*p && *p != ';')
+		p++;
+	while(*p == ';' || (isascii(*p) && isspace(*p)))
+		p++;
+	return p;
+}
+
+static char*
+getstring(char *p, char *s, char *e, int dolower)
+{
+	int c;
+
+	p = skipwhite(p);
+	if(*p == '"'){
+		for(p++; (c = *p) != '"'; p++){
+			if(c == '\\')
+				c = *++p;
+			/*
+			 * 821 says <x> after \ can be anything at all.
+			 * we just don't care.
+			 */
+			if(c == 0)
+				break;
+			if(c < ' ')
+				continue;
+			if(dolower && c >= 'A' && c <= 'Z')
+				c += 0x20;
+			s = sputc(s, e, c);
+		}
+		if(*p == '"')
+			p++;
+	}else{
+		for(; (c = *p) && !isspace(c) && c != ';'; p++){
+			if(c == '\\')
+				c = *++p;
+			/*
+			 * 821 says <x> after \ can be anything at all.
+			 * we just don't care.
+			 */
+			if(c == 0)
+				break;
+			if(c < ' ')
+				continue;
+			if(dolower && c >= 'A' && c <= 'Z')
+				c += 0x20;
+			s = sputc(s, e, c);
+		}
+	}
+	*s = 0;
+	return p;
+}
+
+static void
+setfilename(Message *m, char *p)
+{
+	char buf[Pathlen];
+
+	dprint("setfilename %p %s -> %s\n", m, m->filename, p);
+	if(m->filename != nil)
+		return;
+	getstring(p, buf, buf + sizeof buf - 1, 0);
+	m->filename = smprint("%s", buf);
+	for(p = m->filename; *p; p++)
+		if(*p == ' ' || *p == '\t' || *p == ';')
+			*p = '_';
+}
+
+static char*
+rtrim(char *p)
+{
+	char *e;
+
+	if(p == 0)
+		return p;
+	e = p + strlen(p) - 1;
+	while(e > p && isascii(*e) && isspace(*e))
+		*e-- = 0;
+	return p;
+}
+
+static char*
+unfold(char *s)
+{
+	char *p, *q;
+
+	q = s;
+	for(p = q; *p != '\0'; p++)
+		if(*p != '\r' && *p != '\n')
+			*q++ = *p;
+	*q = '\0';
+	return s;
+}
+
+static char*
+addr822(char *p, char **ac)
+{
+	int n, c, space, incomment, addrdone, inanticomment, quoted;
+	char s[128+1], *ps, *e, *x, *list;
+
+	list = 0;
+	s[0] = 0;
+	ps = s;
+	e = s + sizeof s;
+	space = quoted = incomment = addrdone = inanticomment = 0;
+	n = 0;
+	for(; c = *p; p++){
+		if(!inanticomment && !quoted && !space && ps != s && c == ' '){
+			ps = sputc(ps, e, c);
+			space = 1;
+			continue;
+		}
+		space = 0;
+		if(!quoted && isspace(c) || c == '\r')
+			continue;
+		/* strings are always treated as atoms */
+		if(!quoted && c == '"'){
+			if(!addrdone && !incomment && !ac)
+				ps = sputc(ps, e, c);
+			for(p++; c = *p; p++){
+				if(ac && c == '"')
+					break;
+				if(!addrdone && !incomment && c != '\r' && c != '\n')
+					ps = sputc(ps, e, c);
+				if(!quoted && *p == '"')
+					break;
+				if(*p == '\\')
+					quoted = 1;
+				else
+					quoted = 0;
+			}
+			if(c == 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){
+			if(ac){
+				*ps-- = 0;
+				if(ps > s && *ps == ' ')
+					*ps = 0;
+				if(*ac){
+					*ac = smprint("%s, %s", x=*ac, s);
+					free(x);
+				}else
+					*ac = smprint("%s", s);
+			}
+
+			inanticomment = 1;
+			ps = s;
+			continue;
+		}
+		if(!quoted && c == '>' && inanticomment){
+			addrdone = 1;
+			inanticomment = 0;
+			continue;
+		}
+
+		/* commas separate addresses */
+		if(!quoted && c == ',' && !inanticomment){
+			*ps = 0;
+			addrdone = 0;
+			if(n++ != 0){
+				list = smprint("%s %s", x=list, s);
+				free(x);
+			}else
+				list = smprint("%s", s);
+			ps = s;
+			continue;
+		}
+
+		/* what's left is part of the address */
+		ps = sputc(ps, e, c);
+
+		/* quoted characters are recognized only as characters */
+		if(c == '\\')
+			quoted = 1;
+		else
+			quoted = 0;
+
+	}
+
+	if(ps > s){
+		*ps = 0;
+		if(n != 0){
+			list = smprint("%s %s", x=list, s);
+			free(x);
+		}else
+			list = smprint("%s", s);
+	}
+	return rtrim(list);
+}
+
+/*
+ * per rfc2822 §4.5.3, permit multiple to, cc and bcc headers by
+ * concatenating their values.
+ */
+
+static char*
+concat822(Message*, Header *h, char *o, char *p)
+{
+	char *s, *n;
+
+	p += strlen(h->type);
+	s = addr822(p, 0);
+	if(o){
+		n = smprint("%s %s", o, s);
+		free(s);
+	}else
+		n = s;
+	return n;
+}
+
+static char*
+from822(Message *m, Header *h, char*, char *p)
+{
+	if(m->ffrom)
+		free(m->ffrom);
+	m->from = 0;
+	return addr822(p + h->len, &m->ffrom);
+}
+
+static char*
+replace822(Message *, Header *h, char*, char *p)
+{
+	return addr822(p + h->len, 0);
+}
+
+static char*
+copy822(Message*, Header *h, char*, char *p)
+{
+	return rtrim(unfold(strdup(skipwhite(p + h->len))));
+}
+
+static char*
+ref822(Message *m, Header *h, char*, char *p)
+{
+	char **a, *s, *f[Nref + 1];
+	int i, j, n;
+
+	s = strdup(skipwhite(p + h->len));
+	n = getfields(s, f, nelem(f), 1, "<> \n\t\r,");
+	if(n > Nref)
+		n = Nref;
+	/*
+	 * if there are too many references, drop from the beginning
+	 * of the list. If someone else has a duplicate, we keep the
+	 * old duplicate.
+	 */
+	a = m->references;
+	for(i = 0; i < n; i++){
+		for(j = 0; j < Nref; j++)
+			if(a[j] == nil || strcmp(a[j], f[i]) == 0)
+				break;
+		if(j == Nref){
+			free(a[0]);
+			memmove(&a[0], &a[1], (Nref - 1) * sizeof(a[0]));
+			j--;
+		} else if(a[j] != nil)
+			continue;
+		a[j] = strdup(f[i]);
+	}
+	free(s);
+	return (char*)~0;
+}
+
+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 char*
+ctype(Message *m, Header *h, char*, char *p)
+{
+	char buf[128], *e;
+
+	e = buf + sizeof buf - 1;
+	p = getstring(skipwhite(p + h->len), buf, e, 1);
+	m->type = intern(buf);
+
+	for(; *p; p = skiptosemi(p))
+		if(isattribute(&p, "boundary")){
+			p = getstring(p, buf, e, 0);
+			free(m->boundary);
+			m->boundary = smprint("--%s", buf);
+		} 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")){
+			setfilename(m, p);
+		} else if(isattribute(&p, "charset")){
+			p = getstring(p, buf, e, 1);
+			m->charset = intern(buf);
+		}
+	return (char*)~0;
+}
+
+static char*
+cencoding(Message *m, Header *h, char*, char *p)
+{
+	p = skipwhite(p + h->len);
+	if(cistrncmp(p, "base64", 6) == 0)
+		m->encoding = Ebase64;
+	else if(cistrncmp(p, "quoted-printable", 16) == 0)
+		m->encoding = Equoted;
+	return (char*)~0;
+}
+
+static char*
+cdisposition(Message *m, Header *h, char*, char *p)
+{
+	for(p = skipwhite(p + h->len); *p; p = skiptosemi(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);
+		}
+	return (char*)~0;
+}
+
+ulong	msgallocd;
+ulong	msgfreed;
+
+Message*
+newmessage(Message *parent)
+{
+	static int id;
+	Message *m;
+
+	msgallocd++;
+
+	m = emalloc(sizeof *m);
+	dprint("newmessage %ld	%p	%p\n", msgallocd, parent, m);
+	m->type = intern("text/plain");
+	m->charset = intern("iso-8859-1");
+	m->cstate = Cidxstale;
+	m->flags = Frecent;
+	m->id = newid();
+	if(parent)
+		snprint(m->name, sizeof m->name, "%d", ++(parent->subname));
+	if(parent == nil)
+		parent = m;
+	m->whole = parent;
+	m->hlen = -1;
+	return m;
+}
+
+/* delete a message from a mailbox */
+static void
+delmessage(Mailbox *mb, Message *m)
+{
+	Message **l;
+
+	assert(m->refs == 0);
+	while(m->part)
+		delmessage(mb, m->part);
+
+	mb->vers++;
+	msgfreed++;
+
+	if(m != m->whole){
+		/* unchain from parent */
+		for(l = &m->whole->part; *l && *l != m; l = &(*l)->next)
+			;
+		if(*l != nil)
+			*l = m->next;
+		m->next = nil;
+		/* 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);
+		hfree(PATH(m->id, Qdir), "xxx");		/* sleezy speedup */
+
+		if(Topmsg(mb, m))
+			mtreedelete(mb, m);
+		cachefree(mb, m);
+		idxfree(m);
+	}
+	free(m);
+}
+
+void
+unnewmessage(Mailbox *mb, Message *parent, Message *m)
+{
+	assert(parent->subname > 0);
+	m->deleted = Dup;
+	delmessage(mb, m);
+	parent->subname -= 1;
+}
+
+/* mark messages (identified by path) for deletion */
+char*
+delmessages(int ac, char **av)
+{
+	int i, needwrite;
+	Mailbox *mb;
+	Message *m;
+
+	for(mb = mbl; mb != nil; mb = mb->next)
+		if(strcmp(av[0], mb->name) == 0)
+			break;
+	if(mb == nil)
+		return "no such mailbox";
+
+	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){
+					needwrite = 1;
+					m->deleted = Deleted;
+					logmsg(m, "deleting");
+				}
+				break;
+			}
+	if(needwrite)
+		syncmbox(mb, 1);
+	return 0;
+}
+
+char*
+flagmessages(int argc, char **argv)
+{
+	char *err, *rerr;
+	int i, needwrite;
+	Mailbox *mb;
+	Message *m;
+
+	if(argc%2)
+		return "bad flags";
+	for(mb = mbl; mb != nil; mb = mb->next)
+		if(strcmp(*argv, mb->name) == 0)
+			break;
+	if(mb == nil)
+		return "no such mailbox";
+	needwrite = 0;
+	rerr = 0;
+	for(i = 1; i < argc; i += 2)
+		for(m = mb->root->part; m; m = m->next)
+			if(strcmp(m->name, argv[i]) == 0){
+				if(err = modflags(mb, m, argv[i + 1]))
+					rerr = err;
+				else
+					needwrite = 1;
+			}
+	if(needwrite)
+		syncmbox(mb, 1);
+	return rerr;
+}
+
+char*
+movemessages(int argc, char **argv)
+{
+	char *err, *dest, *rerr;
+	int i, needwrite;
+	Mailbox *mb;
+	Message *m;
+
+	rerr = 0;
+	for(mb = mbl; mb != nil; mb = mb->next)
+		if(strcmp(*argv, mb->name) == 0)
+			break;
+	if(mb == nil)
+		return "no such mailbox";
+	if(mb->move == nil)
+		return "move not supported";
+	dest = argv[argc - 1];
+	needwrite = 0;
+	for(i = 1; i < argc - 1; i++)
+		for(m = mb->root->part; m; m = m->next)
+			if(strcmp(m->name, argv[i]) == 0){
+				if(err = mb->move(mb, m, dest))
+					rerr = err;
+				else
+					needwrite = 1;
+			}
+	if(needwrite)
+		syncmbox(mb, 1);
+	return rerr;
+}
+
+void
+msgincref(Mailbox *mb, Message *m)
+{
+	assert(mb->refs >= 0);
+	for(;; m = m->whole){
+		assert(m->refs >= 0);
+		m->refs++;
+		if(Topmsg(mb, m))
+			break;
+	}
+}
+
+void
+msgdecref(Mailbox *mb, Message *m)
+{
+	assert(mb->refs >= 0);
+	for(;; m = m->whole){
+		assert(m->refs > 0);
+		m->refs--;
+		if(Topmsg(mb, m)){
+			if(m->refs == 0){
+				if(m->deleted)
+					syncmbox(mb, 1);
+				else
+					putcache(mb, m);
+			}
+			break;
+		}
+	}
+}
+
+void
+mboxincref(Mailbox *mb)
+{
+	assert(mb->refs > 0);
+	mb->refs++;
+}
+
+static void
+mbrmidx(char *path, int flags)
+{
+	char buf[Pathlen];
+
+	snprint(buf, sizeof buf, "%s.idx", path);
+	vremove(buf);
+	if((flags & Rtrunc) == 0){
+		snprint(buf, sizeof buf, "%s.imp", path);
+		vremove(buf);
+	}
+}
+
+void
+mboxdecref(Mailbox *mb)
+{
+	assert(mb->refs > 0);
+	if(--mb->refs)
+		return;
+	syncmbox(mb, 1);
+	delmessage(mb, mb->root);
+	if(mb->close)
+		mb->close(mb);
+	if(mb->flags & ORCLOSE && mb->remove)
+	if(mb->remove(mb, mb->rmflags))
+		rmidx(mb->path, mb->rmflags);
+	mtreefree(mb);
+	free(mb->d);
+	free(mb);
+}
+
+
+/* just space over \r.  sleezy but necessary for ms email. */
+int
+deccr(char *x, int len)
+{
+	char *e;
+
+	e = x + len;
+	for(;;){
+		x = memchr(x, '\r', e - x);
+		if(x == nil)
+			break;
+		*x = ' ';
+	}
+	return len;
+}
+
+/*
+ *  undecode message body
+ */
+void
+decode(Message *m)
+{
+	int i, len;
+	char *x;
+
+	if(m->decoded)
+		return;
+	dprint("decode %d %p\n", m->encoding, m);
+	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(len == -1){
+			free(x);
+			break;
+		}
+		if(strncmp(m->type, "text/", 5) == 0)
+			len = deccr(x, 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, 0);
+		if(m->ballocd)
+			free(m->body);
+		m->body = x;
+		m->bend = x + len;
+		m->ballocd = 1;
+		break;
+	default:
+		break;
+	}
+	m->decoded = 1;
+}
+
+/* convert x to utf8 */
+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;
+	dprint("convert type=%q charset=%q %p\n", m->type, m->charset, m);
+	m->converted = 1;
+	if(m->part != nil || strncmp(m->type, "text", 4) != 0 || *m->charset == 0)
+		return;
+	len = xtoutf(m->charset, &x, m->body, m->bend);
+	if(len > 0){
+		if(m->ballocd)
+			free(m->body);
+		m->body = x;
+		m->bend = x + len;
+		m->ballocd = 1;
+	}
+}
+
+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 -1;
+}
+
+/*
+ *  underscores are translated in 2047 headers (uscores=1)
+ *  but not in the body (uscores=0)
+ */
+static char*
+decquotedline(char *out, char *in, char *e, int uscores)
+{
+	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(c){
+		case '_':
+			if(uscores){
+				*out++ = ' ';
+				break;
+			}
+		default:
+			*out++ = c;
+			break;
+		case '=':
+			c = hex2int(*in++)<<4;
+			c |= hex2int(*in++);
+			if(c != -1)
+				*out++ = c;
+			else{
+				*out++ = '=';
+				in -= 2;
+			}
+			break;
+		}
+	}
+	if(!soft)
+		*out++ = '\n';
+	*out = 0;
+
+	return out;
+}
+
+int
+decquoted(char *out, char *in, char *e, int uscores)
+{
+	char *p, *nl;
+
+	p = out;
+	while((nl = strchr(in, '\n')) != nil && nl < e){
+		p = decquotedline(p, in, nl, uscores);
+		in = nl + 1;
+	}
+	if(in < e)
+		p = decquotedline(p, in, e - 1, uscores);
+
+	/* make sure we end with a new line */
+	if(*(p - 1) != '\n'){
+		*p++ = '\n';
+		*p = 0;
+	}
+
+	return p - out;
+}
+
+char*
+lowercase(char *p)
+{
+	char *op;
+	int c;
+
+	for(op = p; c = *p; p++)
+		if(isupper(c))
+			*p = tolower(c);
+	return op;
+}
+
+/* translate latin1 directly since it fits neatly in utf */
+static int
+latin1toutf(char **out, char *in, char *e)
+{
+	int n;
+	char *p;
+	Rune r;
+
+	n = 0;
+	for(p = in; p < e; p++)
+		if(*p & 0x80)
+			n++;
+	if(n == 0)
+		return 0;
+
+	n += e - in;
+	*out = p = malloc(n + 1);
+	if(p == nil)
+		return 0;
+
+	for(; in < e; in++){
+		r = (uchar)*in;
+		p += runetochar(p, &r);
+	}
+	*p = 0;
+	return p - *out;
+}
+
+/* translate any thing using the tcs program */
+int
+xtoutf(char *charset, char **out, char *in, char *e)
+{
+	char *av[4], *p;
+	int totcs[2], fromtcs[2], n, len, sofar;
+
+	/* might not need to convert */
+	if(strcmp(charset, "us-ascii") == 0 || strcmp(charset, "utf-8") == 0)
+		return 0;
+	if(strcmp(charset, "iso-8859-1") == 0)
+		return latin1toutf(out, in, e);
+
+	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)
+		goto error;
+	if(pipe(fromtcs) < 0){
+		close(totcs[0]); close(totcs[1]);
+		goto error;
+	}
+	switch(rfork(RFPROC|RFFDG|RFNOWAIT)){
+	case -1:
+		close(fromtcs[0]); close(fromtcs[1]);
+		close(totcs[0]); close(totcs[1]);
+		goto error;
+	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);
+		exec("/bin/tcs", av);
+		_exits("");
+	default:
+		close(fromtcs[1]); close(totcs[0]);
+		switch(rfork(RFPROC|RFFDG|RFNOWAIT)){
+		case -1:
+			close(fromtcs[0]); close(totcs[1]);
+			goto error;
+		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("");
+		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;
+					p = realloc(p, len + 1);
+					if(p == nil)
+						goto error;
+					*out = p;
+				}
+			}
+			close(fromtcs[0]);
+			break;
+		}
+		break;
+	}
+	if(sofar == 0)
+		goto error;
+	return sofar;
+
+error:
+	free(*out);
+	*out = nil;
+	return 0;
+}
+
+void *
+emalloc(ulong n)
+{
+	void *p;
+
+	p = mallocz(n, 1);
+	if(!p)
+		sysfatal("malloc %lud: %r", n);
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void *
+erealloc(void *p, ulong n)
+{
+	if(n == 0)
+		n = 1;
+	p = realloc(p, n);
+	if(!p)
+		sysfatal("realloc %lud: %r", n);
+	setrealloctag(p, getcallerpc(&p));
+	return p;
+}
+
+int
+myplumbsend(int fd, Plumbmsg *m)
+{
+	char *buf;
+	int n;
+
+	buf = plumbpack(m, &n);
+	if(buf == nil)
+		return -1;
+	n = write(fd, buf, n);
+	free(buf);
+	return n;
+}
+
+static void
+mailplumb(Mailbox *mb, Message *m)
+{
+	char buf[256], dbuf[SHA1dlen*2 + 1], len[10], date[32], *from, *subject;
+	int ai;
+	Plumbmsg p;
+	Plumbattr a[7];
+	static int fd = -1;
+
+	subject = m->subject;
+	if(subject == nil)
+		subject = "";
+
+	from = m->from;
+	if(from == nil)
+		from = "";
+
+	sprint(len, "%lud", m->size);
+	if(biffing && m->inmbox)
+		fprint(2, "[ %s / %s / %s ]\n", from, subject, len);
+	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 = len;
+	a[ai-1].next = &a[ai];
+
+	a[++ai].name = "mailtype";
+	if(!m->inmbox)
+		a[ai].value = "delete";
+	else if(m->cstate & Cmod)
+		a[ai].value = "modify";
+	else
+		a[ai].value = "new";
+	a[ai-1].next = &a[ai];
+
+	snprint(date, sizeof date, "%Δ", m->fileid);
+	a[++ai].name = "date";
+	a[ai].value = date;
+	a[ai-1].next = &a[ai];
+
+	if(m->digest){
+		snprint(dbuf, sizeof dbuf, "%A", m->digest);
+		a[++ai].name = "digest";
+		a[ai].value = dbuf;
+		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;
+
+	myplumbsend(fd, &p);
+}
+
+/*
+ *  count the number of lines in the body (for imap4)
+ */
+ulong
+countlines(Message *m)
+{
+	char *p;
+	ulong i;
+
+	i = 0;
+	for(p = strchr(m->rbody, '\n'); p != nil && p < m->rbend; p = strchr(p + 1, '\n'))
+		i++;
+	return i;
+}
+
+char *logf = "fs";
+
+void
+logmsg(Message *m, char *fmt, ...)
+{
+	char buf[256], *p, *e;
+	va_list args;
+
+	if(!lflag)
+		return;
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s.%d: ", user, getpid());
+	if(m)
+		p = seprint(p, e, "from %s digest %A ",
+			m->from, m->digest);
+	va_start(args, fmt);
+	vseprint(p, e, fmt, args);
+	va_end(args);
+
+	if(Sflag)
+		fprint(2, "%s\n", buf);
+	syslog(Sflag, logf, "%s", buf);
+}
+
+void
+iprint(char *fmt, ...)
+{
+	char buf[256], *p, *e;
+	va_list args;
+
+	if(!iflag)
+		return;
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s.%d: ", user, getpid());
+	va_start(args, fmt);
+	vseprint(p, e, fmt, args);
+	vfprint(2, fmt, args);
+	va_end(args);
+	syslog(Sflag, logf, "%s", buf);
+}
+
+void
+eprint(char *fmt, ...)
+{
+	char buf[256], buf2[256], *p, *e;
+	va_list args;
+
+	e = buf + sizeof buf;
+	p = seprint(buf, e, "%s.%d: ", user, getpid());
+	va_start(args, fmt);
+	vseprint(p, e, fmt, args);
+	e = buf2 + sizeof buf2;
+	p = seprint(buf2, e, "upas/fs: ");
+	vseprint(p, e, fmt, args);
+	va_end(args);
+	syslog(Sflag, logf, "%s", buf);
+	fprint(2, "%s", buf2);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/mdir.c
@@ -1,0 +1,299 @@
+#include "common.h"
+#include "dat.h"
+
+typedef struct {
+	int	debug;
+} Mdir;
+
+#define	mdprint(mdir, ...)	if(mdir->debug) fprint(2, __VA_ARGS__)
+
+static int
+slurp(char *f, char *b, uvlong o, long l)
+{
+	int fd, r;
+
+	if((fd = open(f, OREAD)) == -1)
+		return -1;
+	if(seek(fd, o, 0) == o)
+		r = readn(fd, b, l);
+	else
+		r = 0;
+	close(fd);
+	return r != l ? -1: 0;
+}
+
+static int
+mdirfetch(Mailbox *mb, Message *m, uvlong o, ulong l)
+{
+	char buf[Pathlen];
+	Mdir *mdir;
+
+	mdir = mb->aux;
+	mdprint(mdir, "mdirfetch(%D) ...", m->fileid);
+
+	snprint(buf, sizeof buf, "%s/%D", mb->path, m->fileid);
+	if(slurp(buf, m->start + o, o, l) == -1){
+		logmsg(m, "fetch failed: %r");
+		mdprint(mdir, "%r\n");
+		return -1;
+	}
+	mdprint(mdir, "fetched [%llud, %llud]\n", o, o + l);
+	return 0;
+}
+
+/* must be [0-9]+(\..*)? */
+int
+dirskip(Dir *a, uvlong *uv)
+{
+	char *p;
+
+	if(a->length == 0 || (a->qid.type & QTDIR) != 0)
+		return 1;
+	*uv = strtoul(a->name, &p, 0);
+	if(*uv < 1000000 || *p != '.')
+		return 1;
+	*uv = *uv<<8 | strtoul(p + 1, &p, 10) & 0xFF;
+	if(*p)
+		return 1;
+	return 0;
+}
+
+static int
+vcmp(vlong a, vlong b)
+{
+	a -= b;
+	if(a > 0)
+		return 1;
+	if(a < 0)
+		return -1;
+	return 0;
+}
+
+static int
+dircmp(Dir *a, Dir *b)
+{
+	uvlong x, y;
+
+	if(dirskip(a, &x))
+		x = 0;
+	if(dirskip(b, &y))
+		y = 0;
+	return vcmp(x, y);
+}
+
+static char*
+mdirread(Mdir* mdir, Mailbox* mb)
+{
+	int i, fd, n, c;
+	uvlong uv;
+	Dir *d;
+	Message *m, **ll;
+	static char err[ERRMAX];
+
+	err[0] = '\0';
+	if((fd = open(mb->path, OREAD)) == -1){
+		errstr(err, sizeof err);
+		return err;
+	}
+	if((d = dirfstat(fd)) == nil){
+		errstr(err, sizeof err);
+		close(fd);
+		return err;
+	}
+	if(mb->d){
+		if(d->qid.path == mb->d->qid.path)
+		if(d->qid.vers == mb->d->qid.vers){
+			mdprint(mdir, "\tqids match\n");
+			close(fd);
+			free(d);
+			goto finished;
+		}
+		free(mb->d);
+	}
+	logmsg(nil, "reading %s (mdir)", mb->path);
+	mb->d = d;
+
+	n = dirreadall(fd, &d);
+	close(fd);
+	if(n == -1){
+		errstr(err, sizeof err);
+		return err;
+	}
+
+	qsort(d, n, sizeof *d, (int(*)(void*, void*))dircmp);
+	ll = &mb->root->part;
+	for(i = 0; (m = *ll) != nil || i < n; ){
+		if(i < n && dirskip(d + i, &uv)){
+			i++;
+			continue;
+		}
+		c = -1;
+		if(i >= n)
+			c = 1;
+		else if(m)
+			c = vcmp(uv, m->fileid);
+		mdprint(mdir, "consider %s and %D -> %d\n", i<n? d[i].name: 0, m? m->fileid: 1ull, c);
+		if(c < 0){
+			/* new message */
+			mdprint(mdir, "new: %s\n", d[i].name);
+			if(d[i].length > Maxmsg){
+				mdprint(mdir, "skipping bad size: %llud\n", d[i].length);
+				i++;
+				continue;
+			}
+			m = newmessage(mb->root);
+			m->fileid = uv;
+			m->size = d[i].length;
+			m->inmbox = 1;
+			m->next = *ll;
+			*ll = m;
+			ll = &m->next;
+			i++;
+		}else if(c > 0){
+			/* deleted message; */
+			mdprint(mdir, "deleted: %s (%D)\n", i<n? d[i].name: 0, m? m->fileid: 0);
+			m->inmbox = 0;
+			m->deleted = Disappear;
+			ll = &m->next;
+		}else{
+			//logmsg(*ll, "duplicate %s", d[i].name);
+			ll = &m->next;
+			i++;
+		}
+	}
+	free(d);
+finished:
+	return nil;
+}
+
+static void
+mdirdelete(Mailbox *mb, Message *m)
+{
+	char mpath[Pathlen];
+	Mdir* mdir;
+
+	mdir = mb->aux;
+	snprint(mpath, sizeof mpath, "%s/%D", mb->path, m->fileid);
+	mdprint(mdir, "remove: %s\n", mpath);
+	/* may have been removed by other fs.  just log the error. */
+	if(remove(mpath) == -1)
+		logmsg(m, "remove: %s: %r", mpath);
+	m->inmbox = 0;
+}
+
+static char*
+mdirsync(Mailbox* mb)
+{
+	Mdir *mdir;
+
+	mdir = mb->aux;
+	mdprint(mdir, "mdirsync()\n");
+	return mdirread(mdir, mb);
+}
+
+static char*
+mdirctl(Mailbox *mb, int c, char **v)
+{
+	Mdir *mdir;
+
+	mdir = mb->aux;
+	if(c == 1 && strcmp(*v, "debug") == 0)
+		mdir->debug = 1;
+	else if(c == 1 && strcmp(*v, "nodebug") == 0)
+		mdir->debug = 0;
+	else
+		return "bad mdir control message";
+	return nil;
+}
+
+static void
+mdirclose(Mailbox *mb)
+{
+	free(mb->aux);
+}
+
+static int
+qidcmp(Qid *a, Qid *b)
+{
+	if(a->path != b->path)
+		return 1;
+	return a->vers - b->vers;
+}
+
+/*
+ * .idx strategy. we save the qid.path and .vers
+ * of the mdir directory and the date to the index.
+ * we accept the work done by the other upas/fs if
+ * the index is based on the same (or a newer)
+ * qid.  in any event, we recheck the directory after
+ * the directory is four hours old.
+ */
+static int
+idxr(char *s, Mailbox *mb)
+{
+	char *f[5];
+	long t, δt, n;
+	Dir d;
+
+	n = tokenize(s, f, nelem(f));
+	if(n != 4 || strcmp(f[0], "mdirv1") != 0)
+		return -1;
+	t = strtoul(f[1], 0, 0);
+	δt = time(0) - t;
+	if(δt < 0 || δt > 4*3600)
+		return 0;
+	memset(&d, 0, sizeof d);
+	d.qid.path = strtoull(f[2], 0, 0);
+	d.qid.vers = strtoull(f[3], 0, 0);
+	if(mb->d && qidcmp(&mb->d->qid, &d.qid) >= 0)
+		return 0;
+	if(mb->d == 0)
+		mb->d = emalloc(sizeof d);
+	mb->d->qid = d.qid;
+	mb->d->mtime = t;
+	return 0;
+}
+
+static void
+idxw(Biobuf *b, Mailbox *mb)
+{
+	Qid q;
+
+	memset(&q, 0, sizeof q);
+	if(mb->d)
+		q = mb->d->qid;
+	Bprint(b, "mdirv1 %lud %llud %lud\n", time(0), q.path, q.vers);
+}
+
+char*
+mdirmbox(Mailbox *mb, char *path)
+{
+	int m;
+	Dir *d;
+	Mdir *mdir;
+
+	d = dirstat(path);
+	if(!d && mb->flags & DMcreate){
+		createfolder(getuser(), path);
+		d = dirstat(path);
+	}
+	m = d && (d->mode & DMDIR);
+	free(d);
+	if(!m)
+		return Enotme;
+	snprint(mb->path, sizeof mb->path, "%s", path);
+	mdir = emalloc(sizeof *mdir);
+	mdir->debug = 0;
+	mb->aux = mdir;
+	mb->sync = mdirsync;
+	mb->close = mdirclose;
+	mb->fetch = mdirfetch;
+	mb->delete = mdirdelete;
+	mb->remove = localremove;
+	mb->rename = localrename;
+	mb->idxread = idxr;
+	mb->idxwrite = idxw;
+	mb->ctl = mdirctl;
+	mb->move = nil;
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/mkfile
@@ -1,0 +1,30 @@
+</$objtype/mkfile
+
+TARG=fs
+LIB=../common/libcommon.a$O
+OFILES=\
+	cache.$O\
+	fs.$O\
+	header.$O\
+	idx.$O\
+	imap.$O\
+	mbox.$O\
+	mdir.$O\
+	mtree.$O\
+	plan9.$O\
+	pop3.$O\
+	remove.$O\
+	rename.$O\
+	strtotm.$O\
+	tls.$O\
+
+HFILES=\
+	../common/common.h\
+	dat.h\
+
+</sys/src/cmd/mkone
+<../mkupas
+CFLAGS=$CFLAGS  -I../common
+
+acid:V:
+	$CC -a $CFLAGS fs.c>a$O
--- /dev/null
+++ b/sys/src/cmd/upas/fs/mtree.c
@@ -1,0 +1,63 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+#define messageof(p)	((Message*)(((uchar*)&(p)->digest) - offsetof(Message, digest)))
+
+static int
+mtreecmp(Avl *va, Avl *vb)
+{
+	return memcmp(((Mtree*)va)->digest, ((Mtree*)vb)->digest, SHA1dlen);
+}
+
+void
+mtreeinit(Mailbox *mb)
+{
+	mb->mtree = avlcreate(mtreecmp);
+}
+
+void
+mtreefree(Mailbox *mb)
+{
+	free(mb->mtree);
+	mb->mtree = nil;
+}
+
+Message*
+mtreefind(Mailbox *mb, uchar *digest)
+{
+	Mtree t, *p;
+
+	t.digest = digest;
+	if((p = (Mtree*)avllookup(mb->mtree, &t, 0)) == nil)
+		return nil;
+	return messageof(p);
+}
+
+Message*
+mtreeadd(Mailbox *mb, Message *m)
+{
+	Mtree *old;
+
+	assert(Topmsg(mb, m) && m->digest != nil);
+	if((old = (Mtree*)avlinsert(mb->mtree, m)) == nil)
+		return nil;
+	return messageof(old);
+}
+
+void
+mtreedelete(Mailbox *mb, Message *m)
+{
+	Mtree *old;
+
+	assert(Topmsg(mb, m));
+	if(m->digest == nil)
+		return;
+	if(m->deleted & ~Deleted){
+		old = (Mtree*)avllookup(mb->mtree, m, 0);
+		if(old == nil || messageof(old) != m)
+			return;
+	}
+	old = (Mtree*)avldelete(mb->mtree, m);
+	assert(messageof(old) == m);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/plan9.c
@@ -1,0 +1,419 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+typedef struct {
+	Biobuf	*in;
+	char	*shift;
+} Inbuf;
+
+/*
+ *  parse a Unix style header
+ */
+static int
+memtotm(char *p, int n, Tm *t)
+{
+	char buf[128];
+
+	if(n > sizeof buf - 1)
+		n = sizeof buf -1;
+	memcpy(buf, p, n);
+	buf[n] = 0;
+	return strtotm(buf, t);
+}
+
+static int
+chkunix0(char *s, int n)
+{
+	char *p;
+	Tm tm;
+
+	if(n > 256)
+		return -1;
+	if((p = memchr(s, ' ', n)) == nil)
+		return -1;
+	if(memtotm(p, n - (p - s), &tm) < 0)
+		return -1;
+	if(tmnorm(&tm) < 1000000)
+		return -1;
+	return 0;
+}
+
+static int
+chkunix(char *s, int n)
+{
+	int r;
+
+	r = chkunix0(s, n);
+	if(r == -1)
+		eprint("plan9: warning naked from [%.*s]\n", utfnlen(s, n), s);
+	return r;
+}
+
+static void
+addtomessage(Message *m, char *p, int n)
+{
+	int i, len;
+
+	if(n == 0)
+		return;
+	/* add to message (+1 in malloc is for a trailing NUL) */
+	if(m->lim - m->end < n){
+		if(m->start != nil){
+			i = m->end - m->start;
+			len = (4*(i + n))/3;
+			m->start = erealloc(m->start, len + 1);
+			m->end = m->start + i;
+		} else {
+			len = 2*n;
+			m->start = emalloc(len + 1);
+			m->end = m->start;
+		}
+		m->lim = m->start + len;
+		*m->lim = 0;
+	}
+
+	memmove(m->end, p, n);
+	m->end += n;
+	*m->end = 0;
+}
+
+/*
+ *   read in a single message
+ */
+static int
+okmsg(Mailbox *mb, Message *m, Inbuf *b)
+{
+	char e[ERRMAX], buf[128];
+
+	rerrstr(e, sizeof e);
+	if(strlen(e)){
+		if(fd2path(Bfildes(b->in), buf, sizeof buf) < 0)
+			strcpy(buf, "unknown mailbox");
+		eprint("plan9: error reading %s: %r\n", buf);
+		return -1;
+	}
+	if(m->end == m->start)
+		return -1;
+	if(m->end[-1] == '\n')
+		m->end--;
+	*m->end = 0;
+	m->size = m->end - m->start;
+	if(m->size > Maxmsg)
+		return -1;
+	m->bend = m->rbend = m->end;
+	if(m->digest == nil)
+		digestmessage(mb, m);
+	return 0;
+}
+
+static char*
+inbread(Inbuf *b)
+{
+	if(b->shift)
+		return b->shift;
+	return b->shift = Brdline(b->in, '\n');
+}
+
+void
+inbconsume(Inbuf *b)
+{
+	b->shift = 0;
+}
+
+/*
+ * bug: very long line with From at the buffer break.
+ */
+static int
+readmessage(Mailbox *mb, Message *m, Inbuf *b)
+{
+	char *s, *n;
+	long l, state;
+	int r;
+
+	werrstr("");
+	state = 0;
+	for(;;){
+		s = inbread(b);
+		if(s == 0)
+			break;
+		n = s + (l = Blinelen(b->in)) - 1;
+		if(l >= 28 + 7 && n[0] == '\n')
+		if(strncmp(s, "From ", 5) == 0){
+			r = chkunix(s + 5, l - 5);
+			werrstr("");
+			if(r == 0 && ++state == 2)
+				break;
+		}
+		if(state == 0)
+			return -1;
+		addtomessage(m, s, l);
+		inbconsume(b);
+	}
+	return okmsg(mb, m, b);
+}
+
+/* throw out deleted messages.  return number of freshly deleted messages */
+int
+purgedeleted(Mailbox *mb)
+{
+	Message *m;
+	int newdels;
+
+	/* forget about what's no longer in the mailbox */
+	newdels = 0;
+	for(m = mb->root->part; m != nil; m = m->next){
+		if(m->deleted && m->inmbox){
+			newdels++;
+			m->inmbox = 0;
+		}
+	}
+	return newdels;
+}
+
+static void
+mergemsg(Message *m, Message *x)
+{
+	assert(m->start == 0);
+	m->mallocd = 1;
+	m->inmbox = 1;
+	m->lim = x->lim;
+	m->start = x->start;
+	m->end = x->end;
+	m->bend = x->bend;
+	m->rbend = x->rbend;
+	x->lim = 0;
+	x->start = 0;
+	x->end = 0;
+	x->bend = 0;
+	x->rbend = 0;
+}
+
+/*
+ *   read in the mailbox and parse into messages.
+ */
+static char*
+readmbox(Mailbox *mb, Mlock *lk)
+{
+	char buf[Pathlen];
+	Biobuf *in;
+	Dir *d;
+	Inbuf b;
+	Message *m, **l;
+	static char err[ERRMAX];
+
+	l = &mb->root->part;
+
+	/*
+	 *  open the mailbox.  If it doesn't exist, try the temporary one.
+	 */
+retry:
+	in = Bopen(mb->path, OREAD);
+	if(in == nil){
+		errstr(err, sizeof(err));
+		if(strstr(err, "exist") != 0){
+			snprint(buf, sizeof buf, "%s.tmp", mb->path);
+			if(sysrename(buf, mb->path) == 0)
+				goto retry;
+		}
+		return err;
+	}
+
+	/*
+	 *  a new qid.path means reread the mailbox, while
+	 *  a new qid.vers means read any new messages
+	 */
+	d = dirfstat(Bfildes(in));
+	if(d == nil){
+		Bterm(in);
+		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){
+			Bterm(in);
+			free(d);
+			return nil;
+		}
+		if(d->qid.path == mb->d->qid.path){
+			while(*l != nil)
+				l = &(*l)->next;
+			Bseek(in, mb->d->length, 0);
+		}
+		free(mb->d);
+	}
+	mb->d = d;
+
+	memset(&b, 0, sizeof b);
+	b.in = in;
+	b.shift = 0;
+
+	/*  read new messages */
+	logmsg(nil, "reading %s", mb->path);
+	for(;;){
+		if(lk != nil)
+			syslockrefresh(lk);
+		m = newmessage(mb->root);
+		m->mallocd = 1;
+		m->inmbox = 1;
+		if(readmessage(mb, m, &b) < 0){
+			unnewmessage(mb, mb->root, m);
+			break;
+		}
+		/* merge mailbox versions */
+		while(*l != nil){
+			if(memcmp((*l)->digest, m->digest, SHA1dlen) == 0){
+				if((*l)->start == nil){
+					logmsg(*l, "read indexed");
+					mergemsg(*l, m);
+					unnewmessage(mb, mb->root, m);
+					m = *l;
+				}else{
+					logmsg(*l, "duplicate");
+					m->inmbox = 1;		/* remove it */
+					unnewmessage(mb, mb->root, m);
+					m = nil;
+					l = &(*l)->next;
+				}
+				break;
+			} else {
+				/* old mail no longer in box, mark deleted */
+				logmsg(*l, "disappeared");
+				(*l)->inmbox = 0;
+				(*l)->deleted = Disappear;
+				l = &(*l)->next;
+			}
+		}
+		if(m == nil)
+			continue;
+		parse(mb, m, 0, 0);
+		if(m != *l && m->deleted != Dup){
+			logmsg(m, "new");
+		}
+		/* chain in */
+		*l = m;
+		l = &m->next;
+	}
+	logmsg(nil, "mbox read");
+
+	/* whatever is left has been removed from the mbox, mark deleted */
+	while(*l != nil){
+		(*l)->inmbox = 0;
+		(*l)->deleted = Deleted;
+		l = &(*l)->next;
+	}
+
+	Bterm(in);
+	return nil;
+}
+
+static void
+writembox(Mailbox *mb, Mlock *lk)
+{
+	char buf[Pathlen];
+	int mode, errs;
+	Biobuf *b;
+	Dir *d;
+	Message *m;
+
+	snprint(buf, sizeof buf, "%s.tmp", mb->path);
+
+	/*
+	 * preserve old files permissions, if possible
+	 */
+	mode = Mboxmode;
+	if(d = dirstat(mb->path)){
+		mode = d->mode & 0777;
+		free(d);
+	}
+
+	remove(buf);
+	b = sysopen(buf, "alc", mode);
+	if(b == 0){
+		eprint("plan9: can't write temporary mailbox %s: %r\n", buf);
+		return;
+	}
+
+	logmsg(nil, "writing new mbox");
+	errs = 0;
+	for(m = mb->root->part; m != nil; m = m->next){
+		if(lk != nil)
+			syslockrefresh(lk);
+		if(m->deleted)
+			continue;
+		logmsg(m, "writing");
+		if(Bwrite(b, m->start, m->end - m->start) < 0)
+			errs = 1;
+		if(Bwrite(b, "\n", 1) < 0)
+			errs = 1;
+	}
+	logmsg(nil, "wrote new mbox");
+
+	if(sysclose(b) < 0)
+		errs = 1;
+
+	if(errs){
+		eprint("plan9: error writing temporary mail file\n");
+		return;
+	}
+
+	remove(mb->path);
+	if(sysrename(buf, mb->path) < 0)
+		eprint("plan9: can't rename %s to %s: %r\n",
+			buf, mb->path);
+	if(mb->d != nil)
+		free(mb->d);
+	mb->d = dirstat(mb->path);
+}
+
+char*
+plan9syncmbox(Mailbox *mb)
+{
+	char *rv;
+	Mlock *lk;
+
+	lk = nil;
+	if(mb->dolock){
+		lk = syslock(mb->path);
+		if(lk == nil)
+			return "can't lock mailbox";
+	}
+
+	rv = readmbox(mb, lk);		/* interpolate */
+	if(purgedeleted(mb) > 0)
+		writembox(mb, lk);
+
+	if(lk != nil)
+		sysunlock(lk);
+
+	return rv;
+}
+
+void
+plan9decache(Mailbox*, Message *m)
+{
+	m->lim = 0;
+}
+
+/*
+ *   look to see if we can open this mail box
+ */
+char*
+plan9mbox(Mailbox *mb, char *path)
+{
+	char buf[Pathlen];
+	static char err[Pathlen];
+
+	if(access(path, AEXIST) < 0){
+		errstr(err, sizeof err);
+		snprint(buf, sizeof buf, "%s.tmp", path);
+		if(access(buf, AEXIST) < 0)
+			return err;
+	}
+	mb->sync = plan9syncmbox;
+	mb->remove = localremove;
+	mb->rename = localrename;
+	mb->decache = plan9decache;
+	mb->move = nil;
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/planb.c
@@ -1,0 +1,484 @@
+/*
+ * Plan B (mail2fs) mail box format.
+ *
+ * BUG: this does not reconstruct the
+ * raw text for attachments.  So imap and others
+ * will be unable to access any attachment using upas/fs.
+ * As an aid, we add the path to the message directory
+ * to the message body, so the user could build the path
+ * for any attachment and open it.
+ */
+
+#include "common.h"
+#include <ctype.h>
+#include <plumb.h>
+#include <libsec.h>
+#include "dat.h"
+
+static char*
+parseunix(Message *m)
+{
+	char *s, *p, *q;
+	int l;
+	Tm tm;
+
+	l = m->header - m->start;
+	m->unixheader = smprint("%.*s", l, m->start);
+	s = m->start + 5;
+	if((p = strchr(s, ' ')) == nil)
+		return s;
+	*p = 0;
+	m->unixfrom = strdup(s);
+	*p++ = ' ';
+	if(q = strchr(p, '\n'))
+		*q = 0;
+	if(strtotm(p, &tm) < 0)
+		return p;
+	if(q)
+		*q = '\n';
+	m->fileid = (uvlong)tm2sec(&tm) << 8;
+	return 0;
+}
+
+static int
+readmessage(Message *m, char *msg)
+{
+	int fd, n;
+	char *buf, *name, *p;
+	char hdr[128];
+	Dir *d;
+
+	buf = nil;
+	d = nil;
+	name = smprint("%s/raw", msg);
+	if(name == nil)
+		return -1;
+	if(m->filename != nil)
+		free(m->filename);
+	m->filename = strdup(name);
+	if(m->filename == nil)
+		sysfatal("malloc: %r");
+	fd = open(name, OREAD);
+	if(fd < 0)
+		goto Fail;
+	n = read(fd, hdr, sizeof(hdr)-1);
+	if(n <= 0)
+		goto Fail;
+	hdr[n] = 0;
+	close(fd);
+	fd = -1;
+	p = strchr(hdr, '\n');
+	if(p != nil)
+		*++p = 0;
+	if(strncmp(hdr, "From ", 5) != 0)
+		goto Fail;
+	free(name);
+	name = smprint("%s/text", msg);
+	if(name == nil)
+		goto Fail;
+	fd = open(name, OREAD);
+	if(fd < 0)
+		goto Fail;
+	d = dirfstat(fd);
+	if(d == nil)
+		goto Fail;
+	buf = malloc(strlen(hdr) + d->length + strlen(msg) + 10); /* few extra chars */
+	if(buf == nil)
+		goto Fail;
+	strcpy(buf, hdr);
+	p = buf+strlen(hdr);
+	n = readn(fd, p, d->length);
+	if(n < 0)
+		goto Fail;
+	sprint(p+n, "\n[%s]\n", msg);
+	n += 2 + strlen(msg) + 2;
+	close(fd);
+	free(name);
+	free(d);
+	free(m->start);
+	m->start = buf;
+	m->lim = m->end = p+n;
+	if(*(m->end-1) == '\n')
+		m->end--;
+	*m->end = 0;
+	m->bend = m->rbend = m->end;
+
+	return 0;
+Fail:
+	if(fd >= 0)
+		close(fd);
+	free(name);
+	free(buf);
+	free(d);
+	return -1;
+}
+
+/*
+ * Deleted messages are kept as spam instead.
+ */
+static void
+archive(Message *m)
+{
+	char *dir, *p, *nname;
+	Dir d;
+
+	dir = strdup(m->filename);
+	nname = nil;
+	if(dir == nil)
+		return;
+	p = strrchr(dir, '/');
+	if(p == nil)
+		goto Fail;
+	*p = 0;
+	p = strrchr(dir, '/');
+	if(p == nil)
+		goto Fail;
+	p++;
+	if(*p < '0' || *p > '9')
+		goto Fail;
+	nname = smprint("s.%s", p);
+	if(nname == nil)
+		goto Fail;
+	nulldir(&d);
+	d.name = nname;
+	dirwstat(dir, &d);
+Fail:
+	free(dir);
+	free(nname);
+}
+
+int
+purgembox(Mailbox *mb, int virtual)
+{
+	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 > 0 && m->refs == 0){
+			if(m->inmbox){
+				newdels++;
+				/*
+				 * virtual folders are virtual,
+				 * we do not archive
+				 */
+				if(virtual == 0)
+					archive(m);
+			}
+			delmessage(mb, m);
+		}
+	}
+	return newdels;
+}
+
+static int
+mustshow(char* name)
+{
+	if(isdigit(name[0]))
+		return 1;
+	if(0 && name[0] == 'a' && name[1] == '.')
+		return 1;
+	if(0 && name[0] == 's' && name[1] == '.')
+		return 1;
+	return 0;
+}
+
+static int
+readpbmessage(Mailbox *mb, char *msg, int doplumb, int *nnew)
+{
+	Message *m, **l;
+	char *x, *p;
+
+	m = newmessage(mb->root);
+	m->mallocd = 1;
+	m->inmbox = 1;
+	if(readmessage(m, msg) < 0){
+		unnewmessage(mb, mb->root, m);
+		return -1;
+	}
+	for(l = &mb->root->part; *l != nil; l = &(*l)->next)
+		if(strcmp((*l)->filename, m->filename) == 0 &&
+		    *l != m){
+			if((*l)->deleted < 0)
+				(*l)->deleted = 0;
+			delmessage(mb, m);
+			mb->root->subname--;
+			return -1;
+		}
+	m->header = m->end;
+	if(x = strchr(m->start, '\n'))
+		m->header = x + 1;
+	if(p = parseunix(m))
+		sysfatal("%s:%s naked From in body? [%s]", mb->path, (*l)->filename, p);
+	m->mheader = m->mhend = m->header;
+	parse(mb, m, 0, 0);
+	if(m != *l && m->deleted != Dup){
+		logmsg(m, "new");
+		newcachehash(mb, m, doplumb);
+		putcache(mb, m);
+		nnew[0]++;
+	}
+
+	/* chain in */
+	*l = m;
+	if(doplumb)
+		mailplumb(mb, m, 0);
+	return 0;
+}
+
+static int
+dcmp(Dir *a, Dir *b)
+{
+	char *an, *bn;
+
+	an = a->name;
+	bn = b->name;
+	if(an[0] != 0 && an[1] == '.')
+		an += 2;
+	if(bn[0] != 0 && bn[1] == '.')
+		bn += 2;
+	return strcmp(an, bn);
+}
+
+static char*
+readpbmbox(Mailbox *mb, int doplumb, int *new)
+{
+	char *month, *msg;
+	int fd, i, j, nd, nmd;
+	Dir *d, *md;
+	static char err[ERRMAX];
+
+	fd = open(mb->path, OREAD);
+	if(fd < 0){
+		errstr(err, sizeof err);
+		return err;
+	}
+	nd = dirreadall(fd, &d);
+	close(fd);
+	if(nd > 0)
+		qsort(d, nd, sizeof d[0], (int (*)(void*, void*))dcmp);
+	for(i = 0; i < nd; i++){
+		month = smprint("%s/%s", mb->path, d[i].name);
+		if(month == nil)
+			break;
+		fd = open(month, OREAD);
+		if(fd < 0){
+			fprint(2, "%s: %s: %r\n", argv0, month);
+			free(month);
+			continue;
+		}
+		md = dirfstat(fd);
+		if(md != nil && (md->qid.type & QTDIR) != 0){
+			free(md);
+			md = nil;
+			nmd = dirreadall(fd, &md);
+			for(j = 0; j < nmd; j++)
+				if(mustshow(md[j].name)){
+					msg = smprint("%s/%s", month, md[j].name);
+					readpbmessage(mb, msg, doplumb, new);
+					free(msg);
+				}
+		}
+		close(fd);
+		free(month);
+		free(md);
+		md = nil;
+	}
+	free(d);
+	return nil;
+}
+
+static char*
+readpbvmbox(Mailbox *mb, int doplumb, int *new)
+{
+	char *data, *ln, *p, *nln, *msg;
+	int fd, nr;
+	long sz;
+	Dir *d;
+	static char err[ERRMAX];
+
+	fd = open(mb->path, OREAD);
+	if(fd < 0){
+		errstr(err, sizeof err);
+		return err;
+	}
+	d = dirfstat(fd);
+	if(d == nil){
+		errstr(err, sizeof err);
+		return err;
+	}
+	sz = d->length;
+	free(d);
+	if(sz > 2 * 1024 * 1024){
+		sz = 2 * 1024 * 1024;
+		fprint(2, "upas/fs: %s: bug: folder too big\n", mb->path);
+	}
+	data = malloc(sz+1);
+	if(data == nil){
+		errstr(err, sizeof err);
+		return err;
+	}
+	nr = readn(fd, data, sz);
+	close(fd);
+	if(nr < 0){
+		errstr(err, sizeof err);
+		free(data);
+		return err;
+	}
+	data[nr] = 0;
+
+	for(ln = data; *ln != 0; ln = nln){
+		nln = strchr(ln, '\n');
+		if(nln != nil)
+			*nln++ = 0;
+		else
+			nln = ln + strlen(ln);
+		p = strchr(ln , ' ');
+		if(p != nil)
+			*p = 0;
+		p = strchr(ln, '\t');
+		if(p != nil)
+			*p = 0;
+		p = strstr(ln, "/text");
+		if(p != nil)
+			*p = 0;
+		msg = smprint("/mail/box/%s/msgs/%s", user, ln);
+		if(msg == nil){
+			fprint(2, "upas/fs: malloc: %r\n");
+			continue;
+		}
+		readpbmessage(mb, msg, doplumb, new);
+		free(msg);
+	}
+	free(data);
+	return nil;
+}
+
+static char*
+readmbox(Mailbox *mb, int doplumb, int virt, int *new)
+{
+	char *mberr;
+	int fd;
+	Dir *d;
+	Message *m;
+	static char err[128];
+
+	if(debug)
+		fprint(2, "read mbox %s\n", mb->path);
+	fd = open(mb->path, OREAD);
+	if(fd < 0){
+		errstr(err, sizeof(err));
+		return err;
+	}
+
+	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;
+		}
+		free(mb->d);
+	}
+	close(fd);
+	mb->d = d;
+	mb->vers++;
+	henter(PATH(0, Qtop), mb->name,
+		(Qid){PATH(mb->id, Qmbox), mb->vers, QTDIR}, nil, mb);
+	snprint(err, sizeof err, "reading '%s'", mb->path);
+	logmsg(nil, err, nil);
+
+	for(m = mb->root->part; m != nil; m = m->next)
+		if(m->deleted == 0)
+			m->deleted = -1;
+	if(virt == 0)
+		mberr = readpbmbox(mb, doplumb, new);
+	else
+		mberr = readpbvmbox(mb, doplumb, new);
+
+	/*
+	 * messages removed from the mbox; flag them to go.
+	 */
+	for(m = mb->root->part; m != nil; m = m->next)
+		if(m->deleted < 0 && doplumb){
+			delmessage(mb, m);
+			if(doplumb)
+				mailplumb(mb, m, 1);
+		}
+	logmsg(nil, "mbox read");
+	return mberr;
+}
+
+static char*
+mbsync(Mailbox *mb, int doplumb, int *new)
+{
+	char *rv;
+
+	rv = readmbox(mb, doplumb, 0, new);
+	purgembox(mb, 0);
+	return rv;
+}
+
+static char*
+mbvsync(Mailbox *mb, int doplumb, int *new)
+{
+	char *rv;
+
+	rv = readmbox(mb, doplumb, 1, new);
+	purgembox(mb, 1);
+	return rv;
+}
+
+char*
+planbmbox(Mailbox *mb, char *path)
+{
+	char *list;
+	static char err[64];
+
+	if(access(path, AEXIST) < 0)
+		return Enotme;
+	list = smprint("%s/list", path);
+	if(access(list, AEXIST) < 0){
+		free(list);
+		return Enotme;
+	}
+	free(list);
+	mb->sync = mbsync;
+	if(debug)
+		fprint(2, "planb mbox %s\n", path);
+	return nil;
+}
+
+char*
+planbvmbox(Mailbox *mb, char *path)
+{
+	int fd, nr, i;
+	char buf[64];
+	static char err[64];
+
+	fd = open(path, OREAD);
+	if(fd < 0)
+		return Enotme;
+	nr = read(fd, buf, sizeof(buf)-1);
+	close(fd);
+	if(nr < 7)
+		return Enotme;
+	buf[nr] = 0;
+	for(i = 0; i < 6; i++)
+		if(buf[i] < '0' || buf[i] > '9')
+			return Enotme;
+	if(buf[6] != '/')
+		return Enotme;
+	mb->sync = mbvsync;
+	if(debug)
+		fprint(2, "planb virtual mbox %s\n", path);
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/pop3.c
@@ -1,0 +1,630 @@
+#include "common.h"
+#include <libsec.h>
+#include <auth.h>
+#include "dat.h"
+
+#pragma varargck type "M" uchar*
+#pragma varargck argpos pop3cmd 2
+#define pdprint(p, ...)	if((p)->debug) fprint(2, __VA_ARGS__); else{}
+
+typedef struct Popm Popm;
+struct Popm{
+	int	mesgno;
+};
+
+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;
+
+	Biobuf	bin;		/* open network connection */
+	Biobuf	bout;
+	int	fd;
+	char	*lastline;		/* from Brdstr */
+};
+
+static int
+mesgno(Message *m)
+{
+	Popm *a;
+
+	a = m->aux;
+	return a->mesgno;
+}
+
+static 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");
+	pdprint(pop, "<- %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;
+
+	if((s = Brdstr(&pop->bin, '\n', 0)) == nil){
+		close(pop->fd);
+		pop->fd = -1;
+		return "unexpected eof";
+	}
+
+	p = s + strlen(s) - 1;
+	while(p >= s && (*p == '\r' || *p == '\n'))
+		*p-- = '\0';
+
+	pdprint(pop, "-> %s\n", s);
+	free(pop->lastline);
+	pop->lastline = s;
+	return s;
+}
+
+/*
+ *  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(strcmp(s, "EXPIRE 0") == 0)
+			return "server does not allow mail to be left on server";
+	}
+
+	if(hastls && !pop->notls){
+		pop3cmd(pop, "STLS");
+		if(!isokay(s = pop3resp(pop)))
+			return s;
+		Bterm(&pop->bin);
+		Bterm(&pop->bout);
+		if((pop->fd = wraptls(pop->fd, pop->host)) < 0)
+			return geterrstr();
+		pop->encrypted = 1;
+		Binit(&pop->bin, pop->fd, OREAD);
+		Binit(&pop->bout, pop->fd, OWRITE);
+	}
+	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, utfnlen(buf, 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, "proto=pass service=pop dom=%q%s",
+			pop->host, ubuf);
+		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 && (pop->fd = wraptls(pop->fd, pop->host)) < 0)
+		return geterrstr();
+	pop->encrypted = pop->needssl;
+	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(Mailbox *mb, Pop *pop, Message *m)
+{
+	char *s, *f[3], *wp, *ep;
+	int l, sz, pos, n;
+	Popm *a;
+
+	a = m->aux;
+	if(!pop->pipeline)
+		pop3cmd(pop, "LIST %d", a->mesgno);
+	if(!isokay(s = pop3resp(pop)))
+		return s;
+
+	if(tokenize(s, f, 3) != 3)
+		return "syntax error in LIST response";
+
+	if(atoi(f[1]) != a->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", a->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) {
+			pos = wp - m->start;
+			n = sz/10;
+			if(n < 200)
+				n = 200;
+			sz += n;
+			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;
+	m->size = m->end - m->start;
+	if(m->digest == nil)
+		digestmessage(mb, m);
+
+	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)
+{
+	char *s, *p, *uidl, *f[2];
+	int mno, ignore;
+	Message *m, *next, **l;
+	Popm *a;
+
+	/* 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;
+
+			mno = atoi(f[0]);
+			uidl = f[1];
+			if(strlen(uidl) > 75)	/* RFC 1939 says 70 characters max */
+				continue;
+
+			ignore = 0;
+			while(*l != nil) {
+				a = (*l)->aux;
+				if(strcmp((*l)->idxaux, uidl) == 0){
+					if(a == 0){
+						m = *l;
+						m->mallocd = 1;
+						m->inmbox = 1;
+						m->aux = a = emalloc(sizeof *a);
+					}
+					/* matches mail we already have, note mesgno for deletion */
+					a->mesgno = mno;
+					ignore = 1;
+					l = &(*l)->next;
+					break;
+				}else{
+					/* old mail no longer in box mark deleted */
+					(*l)->inmbox = 0;
+					(*l)->deleted = Deleted;
+					l = &(*l)->next;
+				}
+			}
+			if(ignore)
+				continue;
+
+			m = newmessage(mb->root);
+			m->mallocd = 1;
+			m->inmbox = 1;
+			m->idxaux = strdup(uidl);
+			m->aux = a = emalloc(sizeof *a);
+			a->mesgno = mno;
+
+			/* 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) {
+		(*l)->inmbox = 0;
+		(*l)->deleted = Disappear;
+		l = &(*l)->next;
+	}
+
+	/* download new messages */
+	if(pop->pipeline){
+		switch(rfork(RFPROC|RFMEM)){
+		case -1:
+			eprint("pop3: rfork: %r\n");
+			pop->pipeline = 0;
+
+		default:
+			break;
+
+		case 0:
+			for(m = mb->root->part; m != nil; m = m->next){
+				if(m->start != nil || m->deleted)
+					continue;
+				Bprint(&pop->bout, "LIST %d\r\nRETR %d\r\n", mesgno(m), mesgno(m));
+			}
+			Bflush(&pop->bout);
+			_exits("");
+		}
+	}
+
+	for(m = mb->root->part; m != nil; m = next) {
+		next = m->next;
+
+		if(m->start != nil || m->deleted)
+			continue;
+		if(s = pop3download(mb, pop, m)) {
+			eprint("pop3: download %d: %s\n", mesgno(m), s);
+			unnewmessage(mb, mb->root, m);
+			continue;
+		}
+		parse(mb, m, 1, 0);
+	}
+	if(pop->pipeline)
+		waitpid();
+	return nil;	
+}
+
+/*
+ *  delete marked messages
+ */
+static void
+pop3purge(Pop *pop, Mailbox *mb)
+{
+	Message *m;
+
+	if(pop->pipeline){
+		switch(rfork(RFPROC|RFMEM)){
+		case -1:
+			eprint("pop3: rfork: %r\n");
+			pop->pipeline = 0;
+
+		default:
+			break;
+
+		case 0:
+			for(m = mb->root->part; m != nil; m = m->next){
+				if(m->deleted && m->inmbox)
+					Bprint(&pop->bout, "DELE %d\r\n", mesgno(m));
+			}
+			Bflush(&pop->bout);
+			_exits("");
+		}
+	}
+	for(m = mb->root->part; m != nil; m = m->next) {
+		if(m->deleted && m->inmbox) {
+			if(!pop->pipeline)
+				pop3cmd(pop, "DELE %d", mesgno(m));
+			if(!isokay(pop3resp(pop)))
+				continue;
+			m->inmbox = 0;
+		}
+	}
+}
+
+
+/* connect to pop3 server, sync mailbox */
+static char*
+pop3sync(Mailbox *mb)
+{
+	char *err;
+	Pop *pop;
+
+	pop = mb->aux;
+	if(err = pop3dial(pop))
+		goto out;
+	if((err = pop3read(pop, mb)) == nil)
+		pop3purge(pop, mb);
+	pop3hangup(pop);
+out:
+	mb->waketime = (ulong)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;
+
+	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(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);
+}
+
+static char*
+mkmbox(Pop *pop, char *p, char *e)
+{
+	p = seprint(p, e, "%s/box/%s/pop.%s", MAILROOT, getlog(), pop->host);
+	if(pop->user && strcmp(pop->user, getlog()))
+		p = seprint(p, e, ".%s", pop->user);
+	return p;
+}
+
+/*
+ *  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;
+
+	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;
+	mkmbox(pop, mb->path, mb->path + sizeof mb->path);
+	mb->aux = pop;
+	mb->sync = pop3sync;
+	mb->close = pop3close;
+	mb->ctl = pop3ctl;
+	mb->move = nil;
+	mb->addfrom = 1;
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/ref.c
@@ -1,0 +1,100 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+/* all the data that's fit to cache */
+
+typedef struct{
+	char	*s;
+	int	l;
+	ulong	ref;
+}Refs;
+
+Refs	*rtab;
+int	nrtab;
+int	nralloc;
+
+int
+newrefs(char *s)
+{
+	int l, i;
+	Refs *r;
+
+	l = strlen(s);
+	for(i = 0; i < nrtab; i++){
+		r = rtab + i;
+		if(r->ref == 0)
+			goto enter;
+		if(l == r->l && strcmp(r->s, s) == 0){
+			r->ref++;
+			return i;
+		}
+	}
+	if(nrtab == nralloc)
+		rtab = erealloc(rtab, sizeof *rtab*(nralloc += 50));
+	nrtab = i + 1;
+enter:
+	r = rtab + i;
+	r->s = strdup(s);
+	r->l = l;
+	r->ref = 1;
+	return i;
+}
+
+void
+delrefs(int i)
+{
+	Refs *r;
+
+	r = rtab + i;
+	if(--r->ref > 0)
+		return;
+	free(r->s);
+	memset(r, 0, sizeof *r);
+}
+
+void
+refsinit(void)
+{
+	newrefs("");
+}
+
+static char *sep = "--------\n";
+
+int
+prrefs(Biobuf *b)
+{
+	int i, n;
+
+	n = 0;
+	for(i = 1; i < nrtab; i++){
+		if(rtab[i].ref == 0)
+			continue;
+		Bprint(b, "%s ", rtab[i].s);
+		if(n++%8 == 7)
+			Bprint(b, "\n");
+	}
+	if(n%8 != 7)
+		Bprint(b, "\n");
+	Bprint(b, sep);
+	return 0;
+}
+
+int
+rdrefs(Biobuf *b)
+{
+	char *f[10], *s;
+	int i, n;
+
+	while(s = Brdstr(b, '\n', 1)){
+		if(strcmp(s, sep) == 0){
+			free(s);
+			return 0;
+		}
+		n = tokenize(s, f, nelem(f));
+		for(i = 0; i < n; i++)
+			newrefs(f[i]);
+		free(s);
+	}
+	return -1;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/remove.c
@@ -1,0 +1,141 @@
+#include "common.h"
+#include "dat.h"
+
+#define deprint(...)	/* eprint(__VA_ARGS__) */
+
+extern int dirskip(Dir*, uvlong*);
+
+static int
+ismbox(char *path)
+{
+	char buf[512];
+	int fd, r;
+
+	fd = open(path, OREAD);
+	if(fd == -1)
+		return 0;
+	r = 1;
+	if(read(fd, buf, sizeof buf) < 28 + 5)
+		r = 0;
+	else if(strncmp(buf, "From ", 5))
+		r = 0;
+	close(fd);
+	return r;
+}
+
+static int
+isindex(Dir *d)
+{
+	char *p;
+
+	p = strrchr(d->name, '.');
+	if(!p)
+		return -1;
+	if(strcmp(p, ".idx") || strcmp(p, ".imp"))
+		return 1;
+	return 0;
+}
+
+static int
+idiotcheck(char *path, Dir *d, int getindex)
+{
+	uvlong v;
+
+	if(d->mode & DMDIR)
+		return 0;
+	if(strncmp(d->name, "L.", 2) == 0)
+		return 0;
+	if(getindex && isindex(d))
+		return 0;
+	if(!dirskip(d, &v) || ismbox(path))
+		return 0;
+	return -1;
+}
+
+int
+vremove(char *buf)
+{
+	deprint("rm %s\n", buf);
+	return remove(buf);
+}
+
+static int
+rm(char *dir, int flags, int level)
+{
+	char buf[Pathlen];
+	int i, n, r, fd, isdir, rflag;
+	Dir *d;
+
+	d = dirstat(dir);
+	isdir = d->mode & DMDIR;
+	free(d);
+	if(!isdir)
+		return 0;
+	fd = open(dir, OREAD);
+	if(fd == -1)
+		return -1;
+	n = dirreadall(fd, &d);
+	close(fd);
+	r = 0;
+	rflag = flags & Rrecur;
+	for(i = 0; i < n; i++){
+		snprint(buf, sizeof buf, "%s/%s", dir, d[i].name);
+		if(rflag)
+			r |= rm(buf, flags, level + 1);
+		if(idiotcheck(buf, d + i, level + rflag) == -1)
+			continue;
+		if(vremove(buf) != 0)
+			r = -1;
+	}
+	free(d);
+	return r;
+}
+
+void
+rmidx(char *buf, int flags)
+{
+	char buf2[Pathlen];
+
+	snprint(buf2, sizeof buf2, "%s.idx", buf);
+	vremove(buf2);
+	if((flags & Rtrunc) == 0){
+		snprint(buf2, sizeof buf2, "%s.imp", buf);
+		vremove(buf2);
+	}
+}
+
+char*
+localremove(Mailbox *mb, int flags)
+{
+	char *msg, *path;
+	int r, isdir;
+	Dir *d;
+	static char err[2*Pathlen];
+
+	path = mb->path;
+	if((d = dirstat(path)) == 0){
+		snprint(err, sizeof err, "%s: doesn't exist\n", path);
+		return 0;
+	}
+	isdir = d->mode & DMDIR;
+	free(d);
+	msg = "deleting";
+	if(flags & Rtrunc)
+		msg = "truncating";
+	deprint("%s: %s\n", msg, path);
+
+	/* must match folder.c:/^openfolder */
+	r = rm(path, flags, 0);
+	if((flags & Rtrunc) == 0)
+		r = vremove(path);
+	else if(!isdir)
+		close(r = open(path, OWRITE|OTRUNC));
+
+	rmidx(path, flags);
+
+	if(r == -1){
+		snprint(err, sizeof err, "%s: can't %s\n", path, msg);
+		return err;
+	}
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/rename.c
@@ -1,0 +1,234 @@
+#include "common.h"
+#include "dat.h"
+
+#define deprint(...)	/* eprint(__VA_ARGS__) */
+
+static int
+delivery(char *s)
+{
+	if(strncmp(s, "/mail/fs/", 9) == 0)
+	if((s = strrchr(s, '/')) && strcmp(s + 1, "mbox") == 0)
+		return 1;
+	return 0;
+}
+
+static int
+isdir(char *s)
+{
+	int isdir;
+	Dir *d;
+
+	d = dirstat(s);
+	isdir = d && d->mode & DMDIR;
+	free(d);
+	return isdir;
+}
+
+static int
+docreate(char *file, int perm)
+{
+	int fd;
+	Dir ndir;
+	Dir *d;
+
+	fd = create(file, OREAD, perm);
+	if(fd < 0)
+		return -1;
+	d = dirfstat(fd);
+	if(d == nil)
+		return -1;
+	nulldir(&ndir);
+	ndir.mode = perm;
+	ndir.gid = d->uid;
+	dirfwstat(fd, &ndir);
+	close(fd);
+	return 0;
+}
+
+static int
+rollup(char *s)
+{
+	char *p;
+	int mode;
+
+	if(access(s, 0) == 0)
+		return -1;
+
+	/*
+	 * if we can deliver to this mbox, it needs
+	 * to be read/execable all the way down
+	 */
+	mode = 0711;
+	if(delivery(s))
+		mode = 0755;
+
+	for(p = s; p; p++) {
+		if(*p == '/')
+			continue;
+		p = strchr(p, '/');
+		if(p == 0)
+			break;
+		*p = 0;
+		if(access(s, 0) != 0)
+		if(docreate(s, DMDIR|mode) < 0)
+			return -1;
+		*p = '/';
+	}
+	return 0;
+}
+
+static int
+copyfile(char *a, char *b, int flags)
+{
+	char *s;
+	int fd, fd1, mode, i, m, n, r;
+	Dir *d;
+
+	mode = 0600;
+	if(delivery(b))
+		mode = 0622;
+	fd = open(a, OREAD);
+	fd1 = create(b, OWRITE|OEXCL, DMEXCL|mode);
+	if(fd == -1 || fd1 == -1){
+		close(fd);
+		close(fd1);
+		return -1;
+	}
+	s = malloc(64*1024);
+	i = m = 0;
+	while((n = read(fd, s, sizeof s)) > 0)
+		for(i = 0; i != n; i += m)
+			if((m = write(fd1, s + i, n - i)) == -1)
+				goto lose;
+lose:
+	free(s);
+	close(fd);
+	close(fd1);
+	if(i != m || n != 0)
+		return -1;
+
+	if((flags & Rtrunc) == 0)
+		return vremove(a);
+
+	fd = open(a, ORDWR);
+	if(fd == -1)
+		return -1;
+	r = -1;
+	if(d = dirfstat(fd)){
+		d->length = 0;
+		r = dirfwstat(fd, d);
+		free(d);
+	}
+	return r;
+}
+
+static int
+copydir(char *a, char *b, int flags)
+{
+	char *p, buf[Pathlen], ns[Pathlen], owd[Pathlen];
+	int fd, fd1, len, i, n, r;
+	Dir *d;
+
+	fd = open(a, OREAD);
+	fd1 = create(b, OWRITE|OEXCL, DMEXCL|0777);
+	close(fd1);
+	if(fd == -1 || fd1 == -1){
+		close(fd);
+		return -1;
+	}
+
+	/* fixup mode */
+	if(delivery(b))
+	if(d = dirfstat(fd)){
+		d->mode |= 0777;
+		dirfwstat(fd, d);
+		free(d);
+	}
+
+	getwd(owd, sizeof owd);
+	if(chdir(a) == -1)
+		return -1;
+
+	p = seprint(buf, buf + sizeof buf, "%s/", b);
+	len = buf + sizeof buf - p;
+	n = dirreadall(fd, &d);
+	r = 0;
+	for(i = 0; i < n; i++){
+		snprint(p, len, "%s", d[i].name);
+		if(d->mode & DMDIR){
+			snprint(ns, sizeof ns, "%s/%s", a, d[i].name);
+			r |= copydir(ns, buf, 0);
+			chdir(a);
+		}else
+			r |= copyfile(d[i].name, buf, 0);
+		if(r)
+			break;
+	}
+	free(d);
+
+	if((flags & Rtrunc) == 0)
+		r |= vremove(a);
+
+	chdir(owd);
+	return r;
+}
+
+int
+rename(char *a, char *b, int flags)
+{
+	char *e0, *e1;
+	int fd, r;
+	Dir *d;
+
+	e0 = strrchr(a, '/');
+	e1 = strrchr(b, '/');
+	if(!e0 || !e1 || !e1[1])
+		return -1;
+
+	if(e0 - a == e1 - b)
+	if(strncmp(a, b, e0 - a) == 0)
+	if(!delivery(a) || isdir(a)){
+		fd = open(a, OREAD);
+		if(!(d = dirfstat(fd))){
+			close(fd);
+			return -1;
+		}
+		d->name = e1 + 1;
+		r = dirfwstat(fd, d);
+		deprint("rename %s %s -> %d\n", a, b, r);
+		if(r != -1 && flags & Rtrunc)
+			close(create(a, OWRITE, d->mode));
+		free(d);
+		close(fd);
+		return r;
+	}
+
+	if(rollup(b) == -1)
+		return -1;
+	if(isdir(a))
+		return copydir(a, b, flags);
+	return copyfile(a, b, flags);
+}
+
+char*
+localrename(Mailbox *mb, char *p2, int flags)
+{
+	char *path, *msg;
+	int r;
+	static char err[2*Pathlen];
+
+	path = mb->path;
+	msg = "rename";
+	if(flags & Rtrunc)
+		msg = "move";
+	deprint("localrename %s: %s %s\n", msg, path, p2);
+
+	r = rename(path, p2, flags);
+	if(r == -1){
+		snprint(err, sizeof err, "%s: can't %s\n", path, msg);
+		deprint("localrename %s\n", err);
+		return err;
+	}
+	close(r);
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/seg.c
@@ -1,0 +1,164 @@
+#include "common.h"
+#include <libsec.h>
+#include "dat.h"
+
+/*
+ * unnatural acts with virtual memory
+ */
+
+typedef struct{
+	int	ref;
+	char	*va;
+	long	sz;
+}S;
+
+static S		s[15];		/* 386 only gives 4 */
+static int		nstab = nelem(s);
+static long	ssem = 1;
+//static ulong	thresh = 10*1024*1024;
+static ulong	thresh = 1024;
+
+void*
+segmalloc(ulong sz)
+{
+	int i, j;
+	void *va;
+
+	if(sz < thresh)
+		return emalloc(sz);
+	semacquire(&ssem, 1);
+	for(i = 0; i < nstab; i++)
+		if(s[i].ref == 0)
+			goto found;
+notfound:
+	/* errstr not informative; assume we hit seg limit */
+	for(j = nstab - 1; j >= i; j--)
+		if(s[j].ref)
+			break;
+	nstab = j;
+	semrelease(&ssem, 1);
+	return emalloc(sz);
+found:
+	/*
+	 * the system doesn't leave any room for expansion
+	 */
+	va = segattach(SG_CEXEC, "memory", 0, sz + sz/10 + 4096);
+	if(va == 0)
+		goto notfound;
+	s[i].ref++;
+	s[i].va = va;
+	s[i].sz = sz;
+	semrelease(&ssem, 1);
+	memset(va, 0, sz);
+	return va;
+}
+
+void
+segmfree(void *va)
+{
+	char *a;
+	int i;
+
+	a = va;
+	for(i = 0; i < nstab; i++)
+		if(s[i].va == a)
+			goto found;
+	free(va);
+	return;
+found:
+	semacquire(&ssem, 1);
+	s[i].ref--;
+	s[i].va = 0;
+	s[i].sz = 0;
+	semrelease(&ssem, 1);
+}
+
+void*
+segreallocfixup(int i, ulong sz)
+{
+	char buf[ERRMAX];
+	void *va, *ova;
+
+	rerrstr(buf, sizeof buf);
+	if(strstr(buf, "segments overlap") == 0)
+		sysfatal("segibrk: %r");
+	va = segattach(SG_CEXEC, "memory", 0, sz);
+	if(va == 0)
+		sysfatal("segattach: %r");
+	ova = s[i].va;
+fprint(2, "fix memcpy(%p, %p, %lud)\n", va, ova, s[i].sz);
+	memcpy(va, ova, s[i].sz);
+	s[i].va = va;
+	s[i].sz = sz;
+	segdetach(ova);
+	return va;
+}
+
+void*
+segrealloc(void *va, ulong sz)
+{
+	char *a;
+	int i;
+	ulong sz0;
+
+fprint(2, "segrealloc %p %lud\n", va, sz);
+	if(va == 0)
+		return segmalloc(sz);
+	a = va;
+	for(i = 0; i < nstab; i++)
+		if(s[i].va == a)
+			goto found;
+	if(sz >= thresh)
+	if(a = segmalloc(sz)){
+		sz0 = msize(va);
+		memcpy(a, va, sz0);
+fprint(2, "memset(%p, 0, %lud)\n", a + sz0, sz - sz0);
+		memset(a + sz0, 0, sz - sz0);
+		return a;
+	}
+	return realloc(va, sz);
+found:
+	sz0 = s[i].sz;
+fprint(2, "segbrk(%p, %p)\n", s[i].va, s[i].va + sz);
+	va = segbrk(s[i].va, s[i].va + sz);
+	if(va == (void*)-1 || va < end)
+		return segreallocfixup(i, sz);
+	a = va;
+	if(sz > sz0)
+{
+fprint(2, "memset(%p, 0, %lud)\n", a + sz0, sz - sz0);
+		memset(a + sz0, 0, sz - sz0);
+}
+	s[i].va = va;
+	s[i].sz = sz;
+	return va;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+fprint(2, "emalloc %lud\n", n);
+	p = mallocz(n, 1);
+	if(!p)
+		sysfatal("malloc %lud: %r", n);
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void
+main(void)
+{
+	char *p;
+	int i;
+	ulong sz;
+
+	p = 0;
+	for(i = 0; i < 6; i++){
+		sz = i*512;
+		p = segrealloc(p, sz);
+		memset(p, 0, sz);
+	}
+	segmfree(p);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/strtotm.c
@@ -1,0 +1,28 @@
+#include <u.h>
+#include <libc.h>
+
+int
+strtotm(char *s, Tm *t)
+{
+	char **f, *fmt[] = {
+		"WW MMM DD hh:mm:ss ?Z YYYY",
+		"WW MMM DD hh:mm:ss YYYY",
+		"?WW ?DD ?MMM ?YYYY hh:mm:ss ?Z",
+		"?WW ?DD ?MMM ?YYYY hh:mm:ss",
+		"?WW, DD-?MM-YY",
+		"?DD ?MMM ?YYYY hh:mm:ss ?Z",
+		"?DD ?MMM ?YYYY hh:mm:ss",
+		"?DD-?MM-YY hh:mm:ss ?Z",
+		"?DD-?MM-YY hh:mm:ss",
+		"?DD-?MM-YY",
+		"?MMM/?DD/?YYYY hh:mm:ss ?Z",
+		"?MMM/?DD/?YYYY hh:mm:ss",
+		"?MMM/?DD/?YYYY",
+		nil,
+	};
+
+	for(f = fmt; *f; f++)
+		if(tmparse(t, *f, s, nil, nil) != nil)
+			return 0;
+	return -1;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/fs/tls.c
@@ -1,0 +1,37 @@
+#include "common.h"
+#include <libsec.h>
+#include <auth.h>
+#include "dat.h"
+
+int
+wraptls(int ofd, char *host)
+{
+	Thumbprint *thumb;
+	TLSconn conn;
+	int fd;
+
+	memset(&conn, 0, sizeof conn);
+	conn.serverName = host;
+	fd = tlsClient(ofd, &conn);
+	if(fd < 0){
+		close(ofd);
+		return -1;
+	}
+	if(nocertcheck){
+		syslog(Sflag, logf, "ignoring cert for %s", host);
+		goto skip;
+	}
+	thumb = initThumbprints("/sys/lib/tls/mail", "/sys/lib/tls/mail.exclude", "x509");
+	if(thumb != nil){
+		if(!okCertificate(conn.cert, conn.certlen, thumb)){
+			werrstr("cert for %s not recognized: %r", host);
+			close(fd);
+			fd = -1;
+		}
+		freeThumbprints(thumb);
+	}
+skip:
+	free(conn.cert);
+	free(conn.sessionID);
+	return fd;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/auth.c
@@ -1,0 +1,277 @@
+#include "imap4d.h"
+#include <libsec.h>
+
+static char Ebadch[]	= "can't get challenge";
+static char Ecantstart[]	= "can't initialize mail system: %r";
+static char Ecancel[]	= "client cancelled authentication";
+static char Ebadau[]	= "login failed";
+
+/*
+ * hack to allow smtp forwarding.
+ * hide the peer IP address under a rock in the ratifier FS.
+ */
+void
+enableforwarding(void)
+{
+	char buf[64], peer[64], *p;
+	int fd;
+	ulong now;
+	static ulong last;
+
+	if(remote == nil)
+		return;
+
+	now = time(0);
+	if(now < last + 5*60)
+		return;
+	last = now;
+
+	fd = open("/srv/ratify", ORDWR);
+	if(fd < 0)
+		return;
+	if(mount(fd, -1, "/mail/ratify", MBEFORE, "") == -1){
+		close(fd);
+		return;
+	}
+
+	strncpy(peer, remote, sizeof peer);
+	peer[sizeof peer - 1] = 0;
+	p = strchr(peer, '!');
+	if(p != nil)
+		*p = 0;
+
+	snprint(buf, sizeof buf, "/mail/ratify/trusted/%s#32", peer);
+
+	/*
+	 * 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);
+}
+
+void
+setupuser(AuthInfo *ai)
+{
+	int pid;
+	Waitmsg *w;
+
+	if(ai){
+		strecpy(username, username + sizeof username, ai->cuid);
+		if(auth_chuid(ai, nil) < 0)
+			bye("user auth failed: %r");
+		else {	/* chown network connection */
+			Dir nd;
+			nulldir(&nd);
+			nd.mode = 0660;
+			nd.uid = ai->cuid;
+			dirfwstat(Bfildes(&bin), &nd);
+		}
+		auth_freeAI(ai);
+	}else
+		strecpy(username, username + sizeof username, getuser());
+
+	if(strcmp(username, "none") == 0 || newns(username, 0) == -1)
+		bye("user login failed: %r");
+	if(binupas){
+		if(bind(binupas, "/bin/upas", MREPL) > 0)
+			ilog("bound %s on /bin/upas", binupas);
+		else
+			bye("bind %s failed: %r", binupas);
+	}
+
+	/*
+	 * hack to allow access to outgoing smtp forwarding
+	 */
+	enableforwarding();
+
+	snprint(mboxdir, Pathlen, "/mail/box/%s", username);
+	if(mychdir(mboxdir) < 0)
+		bye("can't open user's mailbox");
+
+	switch(pid = fork()){
+	case -1:
+		bye(Ecantstart);
+		break;
+	case 0:
+if(!strstr(argv0, "8.out"))
+		execl("/bin/upas/fs", "upas/fs", "-np", nil);
+else{
+ilog("using /sys/src/cmd/upas/fs/8.out");
+execl("/sys/src/cmd/upas/fs/8.out", "upas/fs", "-np", nil);
+}
+		_exits(0);
+		break;
+	default:
+		break;
+	}
+	if((w = wait()) == nil || w->pid != pid || w->msg[0] != 0)
+		bye(Ecantstart);
+	free(w);
+}
+
+static char*
+authread(int *len)
+{
+	char *t;
+	int n;
+
+	t = Brdline(&bin, '\n');
+	n = Blinelen(&bin);
+	if(n < 2)
+		return nil;
+	n--;
+	if(t[n-1] == '\r')
+		n--;
+	t[n] = 0;
+	if(n == 0 || strcmp(t, "*") == 0)
+		return nil;
+	*len = n;
+	return t;
+}
+
+static char*
+authresp(void)
+{
+	char *s, *t;
+	int n;
+
+	t = authread(&n);
+	if(t == nil)
+		return nil;
+	s = binalloc(&parsebin, n + 1, 1);
+	n = dec64((uchar*)s, n, t, n);
+	s[n] = 0;
+	return s;
+}
+
+/*
+ * rfc 2195 cram-md5 authentication
+ */
+char*
+cramauth(void)
+{
+	char *s, *t;
+	AuthInfo *ai;
+	Chalstate *cs;
+
+	if((cs = auth_challenge("proto=cram role=server")) == nil)
+		return Ebadch;
+
+	Bprint(&bout, "+ %.*[\r\n", cs->nchal, cs->chal);
+	if(Bflush(&bout) < 0)
+		writeerr();
+
+	s = authresp();
+	if(s == nil)
+		return Ecancel;
+
+	t = strchr(s, ' ');
+	if(t == nil)
+		return Ebadch;
+	*t++ = 0;
+	strncpy(username, s, Userlen);
+	username[Userlen - 1] = 0;
+
+	cs->user = username;
+	cs->resp = t;
+	cs->nresp = strlen(t);
+	if((ai = auth_response(cs)) == nil)
+		return Ebadau;
+	auth_freechal(cs);
+	setupuser(ai);
+	return nil;
+}
+
+char*
+crauth(char *u, char *p)
+{
+	char response[64];
+	AuthInfo *ai;
+	static char nchall[64];
+	static Chalstate *ch;
+
+again:
+	if(ch == nil){
+		if(!(ch = auth_challenge("proto=p9cr role=server user=%q", u)))
+			return Ebadch;
+		snprint(nchall, 64, " encrypt challenge: %s", ch->chal);
+		return nchall;
+	} else {
+		strncpy(response, p, 64);
+		ch->resp = response;
+		ch->nresp = strlen(response);
+		ai = auth_response(ch);
+		auth_freechal(ch);
+		ch = nil;
+		if(ai == nil)
+			goto again;
+		setupuser(ai);
+		return nil;
+	}
+}
+
+char*
+passauth(char *u, char *secret)
+{
+	char response[2*MD5dlen + 1];
+	uchar digest[MD5dlen];
+	AuthInfo *ai;
+	Chalstate *cs;
+
+	if((cs = auth_challenge("proto=cram role=server")) == nil)
+		return Ebadch;
+	hmac_md5((uchar*)cs->chal, strlen(cs->chal),
+		(uchar*)secret, strlen(secret), digest, nil);
+	snprint(response, sizeof(response), "%.*H", MD5dlen, digest);
+	cs->user = u;
+	cs->resp = response;
+	cs->nresp = strlen(response);
+	ai = auth_response(cs);
+	if(ai == nil)
+		return Ebadau;
+	auth_freechal(cs);
+	setupuser(ai);
+	return nil;
+}
+
+static int
+niltokenize(char *buf, int n, char **f, int nelemf)
+{
+	int i, nf;
+
+	f[0] = buf;
+	nf = 1;
+	for(i = 0; i < n - 1; i++)
+		if(buf[i] == 0){
+			f[nf++] = buf + i + 1;
+			if(nf == nelemf)
+				break;
+		}
+	return nf;
+}
+
+char*
+plainauth(char *ch)
+{
+	char buf[256*3 + 2], *f[4];
+	int n, nf;
+
+	if(ch == nil){
+		Bprint(&bout, "+ \r\n");
+		if(Bflush(&bout) < 0)
+			writeerr();
+		ch = authread(&n);
+	}
+	if(ch == nil || strlen(ch) == 0)
+		return Ecancel;
+	n  = dec64((uchar*)buf, sizeof buf, ch, strlen(ch));
+	nf = niltokenize(buf, n, f, nelem(f));
+	if(nf != 3)
+		return Ebadau;
+	return passauth(f[1], f[2]);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/capability
@@ -1,0 +1,37 @@
+status
+u	acl			[rfc2086][rfc4314]
+u	annotate-experiment-1	[rfc5257]
+u	binary			[rfc3516]
+	catenate			[rfc4469]
+	children			[rfc3348]
+u	compress=deflate		[rfc4978]
+	condstore		[rfc4551]
+	context=search		[rfc5267]
+	context=sort		[rfc5267]
+u	convert			[rfc-ietf-lemonade-convert-20.txt]
+	enable			[rfc5161]
+*	esearch			[rfc4731]
+	esort			[rfc5267]
+u	i18nlevel=1		[rfc5255]
+u	i18nlevel=2		[rfc5255]
+u	id			[rfc2971]
+y	idle			[rfc2177]
+u	language			[rfc5255]
+	literal+			[rfc2088]
+	login-referrals		[rfc2221]
+y	logindisabled		[rfc2595][rfc3501]
+	mailbox-referrals		[rfc2193]
+	multiappend		[rfc3502]
+y	namespace		[rfc2342]
+	qresync			[rfc5162]
+y	quota			[rfc2087]
+u	rights=			[rfc4314]			
+	sasl-ir			[rfc4959]
+*	searchres			[rfc5182]
+*	sort			[rfc5256]
+	starttls			[rfc2595][rfc3501]
+n	thread			[rfc5256]
+y	uidplus			[rfc4315]	
+n	unselect			[rfc3691]			
+u	urlauth			[rfc4467]
+	within			[rfc5032]
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/copy.c
@@ -1,0 +1,248 @@
+#include "imap4d.h"
+#include <libsec.h>
+
+int
+copycheck(Box*, Msg *m, int, void *)
+{
+	int fd;
+
+	if(m->expunged)
+		return 0;
+	fd = msgfile(m, "rawunix");
+	if(fd < 0){
+		msgdead(m);
+		return 0;
+	}
+	close(fd);
+	return 1;
+}
+
+static int
+opendeliver(int *pip, char *folder, char *from, long t)
+{
+	char *av[7], buf[32];
+	int i, pid, fd[2];
+
+	if(pipe(fd) != 0)
+		sysfatal("pipe: %r");
+	pid = fork();
+	switch(pid){
+	case -1:
+		return -1;
+	case 0:
+		av[0] = "mbappend";
+		av[1] = folder;
+		i = 2;
+		if(from){
+			av[i++] = "-f";
+			av[i++] = from;
+		}
+		if(t != 0){
+			snprint(buf, sizeof buf, "%ld", t);
+			av[i++] = "-t";
+			av[i++] = buf;
+		}
+		av[i] = 0;
+		close(0);
+		dup(fd[1], 0);
+		if(fd[1] != 0)
+			close(fd[1]);
+		close(fd[0]);
+		exec("/bin/upas/mbappend", av);
+		ilog("exec: %r");
+		_exits("b0rked");
+		return -1;
+	default:
+		*pip = fd[0];
+		close(fd[1]);
+		return pid;
+	}
+}
+
+static int
+closedeliver(int pid, int fd)
+{
+	int nz, wpid;
+	Waitmsg *w;
+
+	close(fd);
+	while(w = wait()){
+		nz = !w->msg || !w->msg[0];
+		wpid = w->pid;
+		free(w);
+		if(wpid == pid)
+			return nz? 0: -1;
+	}
+	return -1;
+}
+
+/*
+ * we're going to all this trouble of fiddling the .imp file for
+ * the target mailbox because we wish to save the flags.  we
+ * should be using upas/fs's flags instead.
+ *
+ * note.  appendmb for mbox fmt wants to lock the directory.  
+ * since the locking is intentionally broken, we could get by
+ * with aquiring the lock before we fire up appendmb and
+ * trust that he doesn't worry if he does acquire the lock.
+ * instead, we'll just do locking around the .imp file.
+ */
+static int
+savemsg(char *dst, int flags, char *head, int nhead, Biobuf *b, long n, Uidplus *u)
+{
+	char *digest, buf[Bufsize + 1], digbuf[Ndigest + 1], folder[Pathlen];
+	uchar shadig[SHA1dlen];
+	int i, fd, pid, nr, ok;
+	DigestState *dstate;
+	Mblock *ml;
+
+	snprint(folder, sizeof folder, "%s/%s", mboxdir, dst);
+	pid = opendeliver(&fd, folder, 0, 0);
+	if(pid == -1)
+		return 0;
+	ok = 1;
+	dstate = sha1(nil, 0, nil, nil);
+	if(nhead){
+		sha1((uchar*)head, nhead, nil, dstate);
+		if(write(fd, head, nhead) != nhead){
+			ok = 0;
+			goto loose;
+		}
+	}
+	while(n > 0){
+		nr = n;
+		if(nr > Bufsize)
+			nr = Bufsize;
+		nr = Bread(b, buf, nr);
+		if(nr <= 0){
+			ok = 0;
+			break;
+		}
+		n -= nr;
+		sha1((uchar*)buf, nr, nil, dstate);
+		if(write(fd, buf, nr) != nr){
+			ok = 0;
+			break;
+		}
+	}
+loose:
+	closedeliver(pid, fd);
+	sha1(nil, 0, shadig, dstate);
+	if(ok){
+		digest = digbuf;
+		for(i = 0; i < SHA1dlen; i++)
+			sprint(digest + 2*i, "%2.2ux", shadig[i]);
+		ml = mblock();
+		if(ml == nil)
+			return 0;
+		ok = appendimp(dst, digest, flags, u) == 0;
+		mbunlock(ml);
+	}
+	return ok;
+}
+
+static int
+copysave(Box*, Msg *m, int, void *vs, Uidplus *u)
+{
+	int ok, fd;
+	vlong length;
+	Biobuf b;
+	Dir *d;
+
+	if(m->expunged)
+		return 0;
+	if((fd = msgfile(m, "rawunix")) == -1){
+		msgdead(m);
+		return 0;
+	}
+	if((d = dirfstat(fd)) == nil){
+		close(fd);
+		return 0;
+	}
+	length = d->length;
+	free(d);
+
+	Binit(&b, fd, OREAD);
+	ok = savemsg(vs, m->flags, 0, 0, &b, length, u);
+	Bterm(&b);
+	close(fd);
+	return ok;
+}
+
+int
+copysaveu(Box *box, Msg *m, int i, void *vs)
+{
+	int ok;
+	Uidplus *u;
+
+	u = binalloc(&parsebin, sizeof *u, 1);
+	ok = copysave(box, m, i, vs, u);
+	*uidtl = u;
+	uidtl = &u->next;
+	return ok;
+}
+
+
+/*
+ * first spool the input into a temorary file,
+ * and massage the input in the process.
+ * then save to real box.
+ */
+/*
+ * copy from bin to bout,
+ * map "\r\n" to "\n" and
+ * return the number of bytes in the mapped file.
+ *
+ * exactly n bytes must be read from the input,
+ * unless an input error occurs.
+ */
+static long
+spool(Biobuf *bout, Biobuf *bin, long n)
+{
+	int c;
+
+	while(n > 0){
+		c = Bgetc(bin);
+		n--;
+		if(c == '\r' && n-- > 0){
+			c = Bgetc(bin);
+			if(c != '\n')
+				Bputc(bout, '\r');
+		}
+		if(c < 0)
+			return -1;
+		if(Bputc(bout, c) < 0)
+			return -1;
+	}
+	if(Bflush(bout) < 0)
+		return -1;
+	return Boffset(bout);
+}
+
+int
+appendsave(char *mbox, int flags, char *head, Biobuf *b, long n, Uidplus *u)
+{
+	int fd, ok;
+	Biobuf btmp;
+
+	fd = imaptmp();
+	if(fd < 0)
+		return 0;
+	Bprint(&bout, "+ Ready for literal data\r\n");
+	if(Bflush(&bout) < 0)
+		writeerr();
+	Binit(&btmp, fd, OWRITE);
+	n = spool(&btmp, b, n);
+	Bterm(&btmp);
+	if(n < 0){
+		close(fd);
+		return 0;
+	}
+
+	seek(fd, 0, 0);
+	Binit(&btmp, fd, OREAD);
+	ok = savemsg(mbox, flags, head, strlen(head), &btmp, n, u);
+	Bterm(&btmp);
+	close(fd);
+	return ok;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/csquery.c
@@ -1,0 +1,40 @@
+#include <u.h>
+#include <libc.h>
+
+/*
+ *  query the connection server
+ */
+char*
+csquery(char *attr, char *val, char *rattr)
+{
+	char token[64 + 4], buf[256], *p, *sp;
+	int fd, n;
+
+	if(val == nil || val[0] == 0)
+		return nil;
+	fd = open("/net/cs", ORDWR);
+	if(fd < 0)
+		return nil;
+	fprint(fd, "!%s=%s", attr, val);
+	seek(fd, 0, 0);
+	snprint(token, sizeof token, "%s=", rattr);
+	for(;;){
+		n = read(fd, buf, sizeof buf - 1);
+		if(n <= 0)
+			break;
+		buf[n] = 0;
+		p = strstr(buf, token);
+		if(p != nil && (p == buf || p[-1] == 0)){
+			close(fd);
+			sp = strchr(p, ' ');
+			if(sp)
+				*sp = 0;
+			p = strchr(p, '=');
+			if(p == nil)
+				return nil;
+			return strdup(p + 1);
+		}
+	}
+	close(fd);
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/date.c
@@ -1,0 +1,54 @@
+#include "imap4d.h"
+
+int
+imap4date(Tm *tm, char *date)
+{
+	if(tmparse(tm, "DD-?MM-YYYY", date, nil, nil) == nil)
+		return 0;
+	return 1;
+}
+
+/*
+ * parse imap4 dates
+ */
+ulong
+imap4datetime(char *date)
+{
+	Tm tm;
+	vlong s;
+
+	s = -1;
+	if(tmparse(&tm, "?DD-?MM-YYYY hh:mm:ss ?Z", date, nil, nil) != nil)
+		s = tmnorm(&tm);
+	else if(tmparse(&tm, "?W, ?DD-?MM-YYYY hh:mm:ss ?Z", date, nil, nil) != nil)
+		s = tmnorm(&tm);
+	if(s > 0 && s < (1ULL<<31))
+		return s;
+	return ~0;
+}
+
+/*
+ * parse dates of formats
+ * [Wkd[,]] DD Mon YYYY HH:MM:SS zone
+ * [Wkd] Mon ( D|DD) HH:MM:SS zone YYYY
+ * plus anything similar
+ * return nil for a failure
+ */
+Tm*
+date2tm(Tm *tm, char *date)
+{
+	char **f, *fmts[] = {
+		"?W, ?DD ?MMM YYYY hh:mm:ss ?Z",
+		"?W ?M ?DD hh:mm:ss ?Z YYYY",
+		"?W, DD-?MM-YY hh:mm:ss ?Z",
+		"?DD ?MMM YYYY hh:mm:ss ?Z",
+		"?M ?DD hh:mm:ss ?Z YYYY",
+		"DD-?MM-YYYY hh:mm:ss ?Z",
+		nil,
+	};
+
+	for(f = fmts; *f; f++)
+		if(tmparse(tm, *f, date, nil, nil) != nil)
+			return tm;
+	return nil;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/debug.c
@@ -1,0 +1,30 @@
+#include "imap4d.h"
+
+char	logfile[28]	= "imap4";
+
+void
+debuglog(char *fmt, ...)
+{
+	char buf[1024];
+	va_list arg;
+
+	if(debug == 0)
+		return;
+	va_start(arg, fmt);
+	vseprint(buf, buf + sizeof buf, fmt, arg);
+	va_end(arg);
+	syslog(0, logfile, "[%s:%d] %s", username, getpid(), buf);
+}
+
+void
+ilog(char *fmt, ...)
+{
+	char buf[1024];
+	va_list arg;
+
+	va_start(arg, fmt);
+	vseprint(buf, buf + sizeof buf, fmt, arg);
+	va_end(arg);
+	syslog(0, logfile, "[%s:%d] %s", username, getpid(), buf);
+
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fetch.c
@@ -1,0 +1,558 @@
+#include "imap4d.h"
+
+char *fetchpartnames[FPmax] =
+{
+	"",
+	"HEADER",
+	"HEADER.FIELDS",
+	"HEADER.FIELDS.NOT",
+	"MIME",
+	"TEXT",
+};
+
+/*
+ * implicitly set the \seen flag.  done in a separate pass
+ * so the .imp file doesn't need to be open while the
+ * messages are sent to the client.
+ */
+int
+fetchseen(Box *box, Msg *m, int uids, void *vf)
+{
+	Fetch *f;
+
+	if(m->expunged)
+		return uids;
+	for(f = vf; f != nil; f = f->next){
+		switch(f->op){
+		case Frfc822:
+		case Frfc822text:
+		case Fbodysect:
+			msgseen(box, m);
+			return 1;
+		}
+	}
+	return 1;
+}
+
+/*
+ * fetch messages
+ *
+ * imap4 body[] requests get translated to upas/fs files as follows
+ *	body[id.header] == id/rawheader file + extra \r\n
+ *	body[id.text] == id/rawbody
+ *	body[id.mime] == id/mimeheader + extra \r\n
+ *	body[id] === body[id.header] + body[id.text]
+*/
+int
+fetchmsg(Box *, Msg *m, int uids, void *vf)
+{
+	char *sep;
+	Fetch *f;
+	Tm tm;
+
+	if(m->expunged)
+		return uids;
+	for(f = vf; f != nil; f = f->next)
+		switch(f->op){
+		case Fflags:
+			break;
+		case Fuid:
+			break;
+		case Finternaldate:
+		case Fenvelope:
+		case Frfc822:
+		case Frfc822head:
+		case Frfc822text:
+		case Frfc822size:
+		case Fbodysect:
+		case Fbodypeek:
+		case Fbody:
+		case Fbodystruct:
+			if(!msgstruct(m, 1)){
+				msgdead(m);
+				return uids;
+			}
+			break;
+		default:
+			bye("bad implementation of fetch");
+			return 0;
+		}
+	if(m->expunged)
+		return uids;
+	if(vf == 0)
+		return 1;
+
+	/*
+	 * note: it is allowed to send back the responses one at a time
+	 * rather than all together.  this is exploited to send flags elsewhere.
+	 */
+	Bprint(&bout, "* %ud FETCH (", m->seq);
+	sep = "";
+	if(uids){
+		Bprint(&bout, "UID %ud", m->uid);
+		sep = " ";
+	}
+	for(f = vf; f != nil; f = f->next){
+		switch(f->op){
+		default:
+			bye("bad implementation of fetch");
+			break;
+		case Fflags:
+			Bprint(&bout, "%sFLAGS (", sep);
+			writeflags(&bout, m, 1);
+			Bprint(&bout, ")");
+			break;
+		case Fuid:
+			if(uids)
+				continue;
+			Bprint(&bout, "%sUID %ud", sep, m->uid);
+			break;
+		case Fenvelope:
+			Bprint(&bout, "%sENVELOPE ", sep);
+			fetchenvelope(m);
+			break;
+		case Finternaldate:
+			Bprint(&bout, "%sINTERNALDATE %#D", sep, date2tm(&tm, m->info[Iunixdate]));
+			break;
+		case Fbody:
+			Bprint(&bout, "%sBODY ", sep);
+			fetchbodystruct(m, &m->head, 0);
+			break;
+		case Fbodystruct:
+			Bprint(&bout, "%sBODYSTRUCTURE ", sep);
+			fetchbodystruct(m, &m->head, 1);
+			break;
+		case Frfc822size:
+			Bprint(&bout, "%sRFC822.SIZE %ud", sep, msgsize(m));
+			break;
+		case Frfc822:
+			f->part = FPall;
+			Bprint(&bout, "%sRFC822", sep);
+			fetchbody(m, f);
+			break;
+		case Frfc822head:
+			f->part = FPhead;
+			Bprint(&bout, "%sRFC822.HEADER", sep);
+			fetchbody(m, f);
+			break;
+		case Frfc822text:
+			f->part = FPtext;
+			Bprint(&bout, "%sRFC822.TEXT", sep);
+			fetchbody(m, f);
+			break;
+		case Fbodysect:
+		case Fbodypeek:
+			Bprint(&bout, "%sBODY", sep);
+			fetchbody(fetchsect(m, f), f);
+			break;
+		}
+		sep = " ";
+	}
+	Bprint(&bout, ")\r\n");
+
+	return 1;
+}
+
+/*
+ * print out section, part, headers;
+ * find and return message section
+ */
+Msg *
+fetchsect(Msg *m, Fetch *f)
+{
+	Bputc(&bout, '[');
+	Bnlist(&bout, f->sect, ".");
+	if(f->part != FPall){
+		if(f->sect != nil)
+			Bputc(&bout, '.');
+		Bprint(&bout, "%s", fetchpartnames[f->part]);
+		if(f->hdrs != nil){
+			Bprint(&bout, " (");
+			Bslist(&bout, f->hdrs, " ");
+			Bputc(&bout, ')');
+		}
+	}
+	Bprint(&bout, "]");
+	return findmsgsect(m, f->sect);
+}
+
+/*
+ * actually return the body pieces
+ */
+void
+fetchbody(Msg *m, Fetch *f)
+{
+	char *s, *t, *e, buf[Bufsize + 2];
+	uint start, stop, pos;
+	int fd, n, nn;
+	Pair p;
+
+	if(m == nil){
+		fetchbodystr(f, "", 0);
+		return;
+	}
+	switch(f->part){
+	case FPheadfields:
+	case FPheadfieldsnot:
+		n = m->head.size + 3;
+		s = emalloc(n);
+		n = selectfields(s, n, m->head.buf, f->hdrs, f->part == FPheadfields);
+		fetchbodystr(f, s, n);
+		free(s);
+		return;
+	case FPhead:
+//ilog("head.size %d", m->head.size);
+		fetchbodystr(f, m->head.buf, m->head.size);
+		return;
+	case FPmime:
+		fetchbodystr(f, m->mime.buf, m->mime.size);
+		return;
+	case FPall:
+		fd = msgfile(m, "rawbody");
+		if(fd < 0){
+			msgdead(m);
+			fetchbodystr(f, "", 0);
+			return;
+		}
+		p = fetchbodypart(f, msgsize(m));
+		start = p.start;
+//ilog("head.size %d", m->head.size);
+		if(start < m->head.size){
+			stop = p.stop;
+			if(stop > m->head.size)
+				stop = m->head.size;
+//ilog("fetch header %ld.%ld (%ld)", start, stop, m->head.size);
+			Bwrite(&bout, m->head.buf + start, stop - start);
+			start = 0;
+			stop = p.stop;
+			if(stop <= m->head.size){
+				close(fd);
+				return;
+			}
+		}else
+			start -= m->head.size;
+		stop = p.stop - m->head.size;
+		break;
+	case FPtext:
+		fd = msgfile(m, "rawbody");
+		if(fd < 0){
+			msgdead(m);
+			fetchbodystr(f, "", 0);
+			return;
+		}
+		p = fetchbodypart(f, m->size);
+		start = p.start;
+		stop = p.stop;
+		break;
+	default:
+		fetchbodystr(f, "", 0);
+		return;
+	}
+
+	/*
+	 * read in each block, convert \n without \r to \r\n.
+	 * this means partial fetch requires fetching everything
+	 * through stop, since we don't know how many \r's will be added
+	 */
+	buf[0] = ' ';
+	for(pos = 0; pos < stop; ){
+		n = Bufsize;
+		if(n > stop - pos)
+			n = stop - pos;
+		n = read(fd, &buf[1], n);
+//ilog("read %ld at %d stop %ld\n", n, pos, stop);
+		if(n <= 0){
+//ilog("must fill %ld bytes\n", stop - pos);
+			fetchbodyfill(stop - pos);
+			break;
+		}
+		e = &buf[n + 1];
+		*e = 0;
+		for(s = &buf[1]; s < e && pos < stop; s = t + 1){
+			t = memchr(s, '\n', e - s);
+			if(t == nil)
+				t = e;
+			n = t - s;
+			if(pos < start){
+				if(pos + n <= start){
+					s = t;
+					pos += n;
+				}else{
+					s += start - pos;
+					pos = start;
+				}
+				n = t - s;
+			}
+			nn = n;
+			if(pos + nn > stop)
+				nn = stop - pos;
+			if(Bwrite(&bout, s, nn) != nn)
+				writeerr();
+//ilog("w %ld at %ld->%ld stop %ld\n", nn, pos, pos + nn, stop);
+			pos += n;
+			if(*t == '\n'){
+				if(t[-1] != '\r'){
+					if(pos >= start && pos < stop)
+						Bputc(&bout, '\r');
+					pos++;
+				}
+				if(pos >= start && pos < stop)
+					Bputc(&bout, '\n');
+				pos++;
+			}
+		}
+		buf[0] = e[-1];
+	}
+	close(fd);
+}
+
+/*
+ * resolve the actual bounds of any partial fetch,
+ * and print out the bounds & size of string returned
+ */
+Pair
+fetchbodypart(Fetch *f, uint size)
+{
+	uint start, stop;
+	Pair p;
+
+	start = 0;
+	stop = size;
+	if(f->partial){
+		start = f->start;
+		if(start > size)
+			start = size;
+		stop = start + f->size;
+		if(stop > size)
+			stop = size;
+		Bprint(&bout, "<%ud>", start);
+	}
+	Bprint(&bout, " {%ud}\r\n", stop - start);
+	p.start = start;
+	p.stop = stop;
+	return p;
+}
+
+/*
+ * something went wrong fetching data
+ * produce fill bytes for what we've committed to produce
+ */
+void
+fetchbodyfill(uint n)
+{
+	while(n-- > 0)
+		if(Bputc(&bout, ' ') < 0)
+			writeerr();
+}
+
+/*
+ * return a simple string
+ */
+void
+fetchbodystr(Fetch *f, char *buf, uint size)
+{
+	Pair p;
+
+	p = fetchbodypart(f, size);
+	Bwrite(&bout, buf + p.start, p.stop - p.start);
+}
+
+char*
+printnlist(Nlist *sect)
+{
+	static char buf[100];
+	char *p;
+
+	for(p = buf; sect; sect = sect->next){
+		p += sprint(p, "%ud", sect->n);
+		if(sect->next)
+			*p++ = '.';
+	}
+	*p = 0;
+	return buf;
+}
+
+/*
+ * find the numbered sub-part of the message
+ */
+Msg*
+findmsgsect(Msg *m, Nlist *sect)
+{
+	uint id;
+
+	for(; sect != nil; sect = sect->next){
+		id = sect->n;
+		for(m = m->kids; m != nil; m = m->next)
+			if(m->id == id)
+				break;
+		if(m == nil)
+			return nil;
+	}
+	return m;
+}
+
+void
+fetchenvelope(Msg *m)
+{
+	Tm tm;
+
+	Bprint(&bout, "(%#D %Z ", date2tm(&tm, m->info[Idate]), m->info[Isubject]);
+	Bimapaddr(&bout, m->from);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->sender);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->replyto);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->to);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->cc);
+	Bputc(&bout, ' ');
+	Bimapaddr(&bout, m->bcc);
+	Bprint(&bout, " %Z %Z)", m->info[Iinreplyto], m->info[Imessageid]);
+}
+
+static int
+Bmime(Biobuf *b, Mimehdr *mh)
+{
+	char *sep;
+
+	if(mh == nil)
+		return Bprint(b, "NIL");
+	sep = "(";
+	for(; mh != nil; mh = mh->next){
+		Bprint(b, "%s%Z %Z", sep, mh->s, mh->t);
+		sep = " ";
+	}
+	Bputc(b, ')');
+	return 0;
+}
+
+static void
+fetchext(Biobuf *b, Header *h)
+{
+	Bputc(b, ' ');
+	if(h->disposition != nil){
+		Bprint(b, "(%Z ", h->disposition->s);
+		Bmime(b, h->disposition->next);
+		Bputc(b, ')');
+	}else
+		Bprint(b, "NIL");
+	Bputc(b, ' ');
+	if(h->language != nil){
+		if(h->language->next != nil)
+			Bmime(b, h->language->next);
+		else
+			Bprint(&bout, "%Z", h->language->s);
+	}else
+		Bprint(b, "NIL");
+}
+
+void
+fetchbodystruct(Msg *m, Header *h, int extensions)
+{
+	uint len;
+	Msg *k;
+
+	if(msgismulti(h)){
+		Bputc(&bout, '(');
+		for(k = m->kids; k != nil; k = k->next)
+			fetchbodystruct(k, &k->mime, extensions);
+		if(m->kids)
+			Bputc(&bout, ' ');
+		Bprint(&bout, "%Z", h->type->t);
+		if(extensions){
+			Bputc(&bout, ' ');
+			Bmime(&bout, h->type->next);
+			fetchext(&bout, h);
+		}
+
+		Bputc(&bout, ')');
+		return;
+	}
+
+	Bputc(&bout, '(');
+	if(h->type != nil){
+		Bprint(&bout, "%Z %Z ", h->type->s, h->type->t);
+		Bmime(&bout, h->type->next);
+	}else
+		Bprint(&bout, "\"text\" \"plain\" NIL");
+
+	Bputc(&bout, ' ');
+	if(h->id != nil)
+		Bprint(&bout, "%Z", h->id->s);
+	else
+		Bprint(&bout, "NIL");
+
+	Bputc(&bout, ' ');
+	if(h->description != nil)
+		Bprint(&bout, "%Z", h->description->s);
+	else
+		Bprint(&bout, "NIL");
+
+	Bputc(&bout, ' ');
+	if(h->encoding != nil)
+		Bprint(&bout, "%Z", h->encoding->s);
+	else
+		Bprint(&bout, "NIL");
+
+	/*
+	 * this is so strange: return lengths for a body[text] response,
+	 * except in the case of a multipart message, when return lengths for a body[] response
+	 */
+	len = m->size;
+	if(h == &m->mime)
+		len += m->head.size;
+	Bprint(&bout, " %ud", len);
+
+	len = m->lines;
+	if(h == &m->mime)
+		len += m->head.lines;
+
+	if(h->type == nil || cistrcmp(h->type->s, "text") == 0)
+		Bprint(&bout, " %ud", len);
+	else if(msgis822(h)){
+		Bputc(&bout, ' ');
+		k = m;
+		if(h != &m->mime)
+			k = m->kids;
+		if(k == nil)
+			Bprint(&bout, "(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL) (\"text\" \"plain\" NIL NIL NIL NIL 0 0) 0");
+		else{
+			fetchenvelope(k);
+			Bputc(&bout, ' ');
+			fetchbodystruct(k, &k->head, extensions);
+			Bprint(&bout, " %ud", len);
+		}
+	}
+
+	if(extensions){
+		Bprint(&bout, " NIL");	/* md5 */
+		fetchext(&bout, h);
+	}
+	Bputc(&bout, ')');
+}
+
+/*
+ * print a list of addresses;
+ * each address is printed as '(' personalname atdomainlist mboxname hostname ')'
+ * the atdomainlist is always NIL
+ */
+int
+Bimapaddr(Biobuf *b, Maddr *a)
+{
+	char *host, *sep;
+
+	if(a == nil)
+		return Bprint(b, "NIL");
+	Bputc(b, '(');
+	sep = "";
+	for(; a != nil; a = a->next){
+		/*
+		 * can't send NIL as hostname, since that is code for a group
+		 */
+		host = a->host? a->host: "";
+		Bprint(b, "%s(%Z NIL %Z %Z)", sep, a->personal, a->box, host);
+		sep = " ";
+	}
+	return Bputc(b, ')');
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fns.h
@@ -1,0 +1,138 @@
+/*
+ * sorted by Edit 4,/^$/|sort -bd +1
+ */
+int	Bimapaddr(Biobuf*, Maddr*);
+int	Bimapmimeparams(Biobuf*, Mimehdr*);
+int	Bnlist(Biobuf*, Nlist*, char*);
+int	Bslist(Biobuf*, Slist*, char*);
+int	Dfmt(Fmt*);
+int	δfmt(Fmt*);
+int	Ffmt(Fmt*);
+int	Xfmt(Fmt*);
+int	Zfmt(Fmt*);
+int	appendsave(char*, int , char*, Biobuf*, long, Uidplus*);
+void	bye(char*, ...);
+int	cdcreate(char*, char*, int, ulong);
+Dir	*cddirstat(char*, char*);
+int	cddirwstat(char*, char*, Dir*);
+int	cdexists(char*, char*);
+int	cdopen(char*, char*, int);
+int	cdremove(char*, char*);
+Mblock	*checkbox(Box*, int );
+void	closebox(Box*, int opened);
+void	closeimp(Box*, Mblock*);
+int	copycheck(Box*, Msg*, int uids, void*);
+int	copysaveu(Box*, Msg*, int uids, void*);
+char	*cramauth(void);
+char	*crauth(char*, char*);
+int	creatembox(char*);
+Tm	*date2tm(Tm*, char*);
+void	debuglog(char*, ...);
+char	*decfs(char*, int, char*);
+char	*decmutf7(char*, int, char*);
+int	deletemsg(Box *, Msgset*);
+void	*emalloc(ulong);
+int	emptyimp(char*);
+void	enableforwarding(void);
+char	*encfs(char*, int, char*);
+char	*encmutf7(char*, int nout, char*);
+void	*erealloc(void*, ulong);
+char	*estrdup(char*);
+int	expungemsgs(Box*, int);
+void	*ezmalloc(ulong);
+void	fetchbody(Msg*, Fetch*);
+void	fetchbodyfill(uint);
+Pair	fetchbodypart(Fetch*, uint);
+void	fetchbodystr(Fetch*, char*, uint);
+void	fetchbodystruct(Msg*, Header*, int);
+void	fetchenvelope(Msg*);
+int	fetchmsg(Box*, Msg *, int, void*);
+Msg	*fetchsect(Msg*, Fetch*);
+int	fetchseen(Box*, Msg*, int, void*);
+void	fetchstructext(Header*);
+Msg	*findmsgsect(Msg*, Nlist*);
+int	formsgs(Box*, Msgset*, uint, int, int (*)(Box*, Msg*, int, void*), void*);
+int	fqid(int, Qid*);
+void	freemsg(Box*, Msg*);
+vlong	getquota(void); 
+void	ilog(char*, ...);
+int	imap4date(Tm*, char*);
+ulong	imap4datetime(char*);
+int	imaptmp(void);
+char	*impname(char*);
+int	inmsgset(Msgset*, uint);
+int	isdotdot(char*);
+int	isprefix(char*, char*);
+int	issuffix(char*, char*);
+int	listboxes(char*, char*, char*);
+char	*loginauth(char*, char*);
+int	lsubboxes(char*, char*, char*);
+char	*maddrstr(Maddr*);
+uint	mapflag(char*);
+uint	mapint(Namedint*, char*);
+int	mblocked(void);
+void	mblockrefresh(Mblock*);
+Mblock	*mblock(void);
+char	*mboxname(char*);
+void	mbunlock(Mblock*);
+Fetch	*mkfetch(int, Fetch*);
+Slist	*mkslist(char*, Slist*);
+Store	*mkstore(int, int, int);
+int	movebox(char*, char*);
+void	msgdead(Msg*);
+int	msgfile(Msg*, char*);
+int	msginfo(Msg*);
+int	msgis822(Header*);
+int	msgismulti(Header*);
+int	msgseen(Box*, Msg*);
+uint	msgsize(Msg*);
+int	msgstruct(Msg*, int top);
+char	*mutf7str(char*);
+int	mychdir(char*);
+int	okmbox(char*);
+Box	*openbox(char*, char*, int);
+int	openlocked(char*, char*, int);
+void	parseerr(char*);
+int	parseimp(Biobuf*, Box*);
+char	*passauth(char*, char*);
+char	*plainauth(char*);
+char	*readfile(int);
+int	removembox(char*);
+int	renamebox(char*, char*, int);
+void	resetcurdir(void);
+Fetch	*revfetch(Fetch*);
+Slist	*revslist(Slist*);
+int	searchmsg(Msg*, Search*, int);
+int	searchld(Search*);
+long	selectfields(char*, long n, char*, Slist*, int);
+void	sendflags(Box*, int uids);
+void	setflags(Box*, Msg*, int f);
+void	setname(char*, ...);
+void	setupuser(AuthInfo*);
+int	storemsg(Box*, Msg*, int, void*);
+char	*strmutf7(char*);
+void	strrev(char*, char*);
+int	subscribe(char*, int);
+int	wrimp(Biobuf*, Box*);
+int	appendimp(char*, char*, int, Uidplus*);
+void	writeerr(void);
+void	writeflags(Biobuf*, Msg*, int);
+
+void	fstreeadd(Box*, Msg*);
+void	fstreedelete(Box*, Msg*);
+Msg	*fstreefind(Box*, int);
+int	fstreecmp(Avl*, Avl*);
+
+#pragma varargck argpos	bye		1
+#pragma varargck argpos	debuglog	1
+#pragma varargck argpos	imap4cmd	2
+#pragma varargck	type	"F"		char*
+#pragma varargck	type	"D"		Tm*
+#pragma varargck	type	"δ"		Tm*
+#pragma varargck	type	"X"		char*
+#pragma varargck	type	"Y"		char*
+#pragma varargck	type	"Z"		char*
+
+#define	MK(t)		((t*)emalloc(sizeof(t)))
+#define	MKZ(t)		((t*)ezmalloc(sizeof(t)))
+#define STRLEN(cs)	(sizeof(cs)-1)
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/folder.c
@@ -1,0 +1,186 @@
+#include "imap4d.h"
+
+static	Mblock	mblck = {
+.fd = -1
+};
+
+static char curdir[Pathlen];
+
+void
+resetcurdir(void)
+{
+	curdir[0] = 0;
+}
+
+int
+mychdir(char *dir)
+{
+	if(strcmp(dir, curdir) == 0)
+		return 0;
+	if(dir[0] != '/' || strlen(dir) > Pathlen)
+		return -1;
+	strcpy(curdir, dir);
+	if(chdir(dir) < 0){
+		werrstr("mychdir failed: %r");
+		return -1;
+	}
+	return 0;
+}
+
+int
+cdcreate(char *dir, char *file, int mode, ulong perm)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return create(file, mode, perm);
+}
+
+Dir*
+cddirstat(char *dir, char *file)
+{
+	if(mychdir(dir) < 0)
+		return nil;
+	return dirstat(file);
+}
+
+int
+cdexists(char *dir, char *file)
+{
+	Dir *d;
+
+	d = cddirstat(dir, file);
+	if(d == nil)
+		return 0;
+	free(d);
+	return 1;
+}
+
+int
+cddirwstat(char *dir, char *file, Dir *d)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return dirwstat(file, d);
+}
+
+int
+cdopen(char *dir, char *file, int mode)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return open(file, mode);
+}
+
+int
+cdremove(char *dir, char *file)
+{
+	if(mychdir(dir) < 0)
+		return -1;
+	return remove(file);
+}
+
+/*
+ * open the one true mail lock file
+ */
+Mblock*
+mblock(void)
+{
+	if(mblck.fd >= 0)
+		bye("mail lock deadlock");
+	mblck.fd = openlocked(mboxdir, "L.mbox", OREAD);
+	if(mblck.fd >= 0)
+		return &mblck;
+	ilog("mblock: %r");
+	return nil;
+}
+
+void
+mbunlock(Mblock *ml)
+{
+	if(ml != &mblck)
+		bye("bad mail unlock");
+	if(ml->fd < 0)
+		bye("mail unlock when not locked");
+	close(ml->fd);
+	ml->fd = -1;
+}
+
+void
+mblockrefresh(Mblock *ml)
+{
+	char buf[1];
+
+	seek(ml->fd, 0, 0);
+	read(ml->fd, buf, 1);
+}
+
+int
+mblocked(void)
+{
+	return mblck.fd >= 0;
+}
+
+char*
+impname(char *name)
+{
+	char *s, buf[Pathlen];
+	int n;
+
+	encfs(buf, sizeof buf, name);
+	n = strlen(buf) + STRLEN(".imp") + 1;
+	s = binalloc(&parsebin, n, 0);
+	if(s == nil)
+		return nil;
+	snprint(s, n, "%s.imp", name);
+	return s;
+}
+
+/*
+ * massage the mailbox name into something valid
+ * eliminates all .', and ..',s, redundatant and trailing /'s.
+ */
+char *
+mboxname(char *s)
+{
+	char *ss, *p;
+
+	ss = mutf7str(s);
+	if(ss == nil)
+		return nil;
+	cleanname(ss);
+	if(!okmbox(ss))
+		return nil;
+	p = binalloc(&parsebin, Pathlen, 0);
+	return encfs(p, Pathlen, ss);
+}
+
+char*
+strmutf7(char *s)
+{
+	char *m;
+	int n;
+
+	n = strlen(s) * Mutf7max + 1;
+	m = binalloc(&parsebin, n, 0);
+	if(m == nil)
+		return nil;
+	return encmutf7(m, n, s);
+}
+
+char*
+mutf7str(char *s)
+{
+	char *m;
+	int n;
+
+	/*
+	 * n = strlen(s) * UTFmax / (2.67) + 1
+	 * UTFmax / 2.67 == 3 / (8/3) == 9 / 8
+	 */
+	n = strlen(s);
+	n = (n * 9 + 7) / 8 + 1;
+	m = binalloc(&parsebin, n, 0);
+	if(m == nil)
+		return nil;
+	return decmutf7(m, n, s);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fsenc.c
@@ -1,0 +1,98 @@
+/*
+ * more regrettable, goofy processing
+ */
+#include "imap4d.h"
+
+char tab[0x7f] = {
+['\t']	'0',
+[' ']	'#',
+['#']	'1',
+};
+
+char itab[0x7f] = {
+['0']	'\t',
+['#']	' ',
+['1']	'#',
+};
+
+char*
+encfs(char *buf, int n, char *s)
+{
+	char *p, c;
+
+	if(!s){
+		*buf = 0;
+		return 0;
+	}
+	if(!cistrcmp(s, "inbox"))
+		s = "mbox";
+	for(p = buf; n > 0 && (c = *s++); n--){
+		if(tab[c & 0x7f]){
+			if(n < 1)
+				break;
+			if((c = tab[c]) == 0)
+				break;
+			*p++ = '#';
+		}
+		*p++ = c;
+	}
+	*p = 0;
+	return buf;
+}
+
+char*
+decfs(char *buf, int n, char *s)
+{
+	char *p, c;
+
+	if(!s){
+		*buf = 0;
+		return 0;
+	}
+	if(!cistrcmp(s, "mbox"))
+		s = "INBOX";
+	for(p = buf; n > 0 && (c = *s++); n--){
+		if(c == '#'){
+			c = *s++;
+			if((c = itab[c]) == 0)
+				break;
+		}
+		*p++ = c;
+	}
+	*p = 0;
+	return buf;
+}
+
+/*
+void
+usage(void)
+{
+	fprint(2, "usage: encfs [-d] ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char buf[1024];
+	int dflag;
+	char *(*f)(char*, int, char*);
+
+	dflag = 0;
+	ARGBEGIN{
+	case 'd':
+		dflag ^= 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+	f = encfs;
+	if(dflag)
+		f = decfs;
+	while(*argv){
+		f(buf, sizeof buf, *argv++);
+		print("%s\n", buf);
+	}
+	exits("");
+}
+*/
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/fstree.c
@@ -1,0 +1,58 @@
+#include "imap4d.h"
+
+int
+fstreecmp(Avl *va, Avl *vb)
+{
+	int i;
+	Fstree *a, *b;
+
+	a = (Fstree*)va;
+	b = (Fstree*)vb;
+	i = a->m->id - b->m->id;
+	if(i > 0)
+		i = 1;
+	if(i < 0)
+		i = -1;
+	return i;
+}
+
+Msg*
+fstreefind(Box *mb, int id)
+{
+	Msg m0;
+	Fstree t, *p;
+
+	memset(&t, 0, sizeof t);
+	m0.id = id;
+	t.m = &m0;
+	if(p = (Fstree*)avllookup(mb->fstree, &t, 0))
+		return p->m;
+	return nil;
+}
+
+void
+fstreeadd(Box *mb, Msg *m)
+{
+	Avl *old;
+	Fstree *p;
+
+	assert(m->id > 0);
+	p = ezmalloc(sizeof *p);
+	p->m = m;
+	old = avlinsert(mb->fstree, p);
+	assert(old == 0);
+}
+
+void
+fstreedelete(Box *mb, Msg *m)
+{
+	Fstree t, *p;
+
+	memset(&t, 0, sizeof t);
+	t.m = m;
+	assert(m->id > 0);
+	p = (Fstree*)avldelete(mb->fstree, &t);
+	if(p == nil)
+		_assert("fstree delete fails");
+	free(p);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/imap4d.c
@@ -1,0 +1,2295 @@
+#include "imap4d.h"
+
+/*
+ * these should be in libraries
+ */
+char	*csquery(char *attr, char *val, char *rattr);
+
+/*
+ * implemented:
+ * /lib/rfc/rfc3501	imap4rev1
+ * /lib/rfc/rfc2683	implementation advice
+ * /lib/rfc/rfc2342	namespace capability
+ * /lib/rfc/rfc2222	security protocols
+ * /lib/rfc/rfc1731	security protocols
+ * /lib/rfc/rfc2177	idle capability
+ * /lib/rfc/rfc2195	cram-md5 authentication
+ * /lib/rfc/rfc4315	uidplus capability
+ *
+ * not implemented, priority:
+ * /lib/rfc/rfc5256	sort and thread
+ *	requires missing support from upas/fs.
+ *
+ * not implemented, low priority:
+ * /lib/rfc/rfc2088	literal+ capability
+ * /lib/rfc/rfc2221	login-referrals
+ * /lib/rfc/rfc2193	mailbox-referrals
+ * /lib/rfc/rfc1760	s/key authentication
+ *
+ */
+
+typedef struct	Parsecmd	Parsecmd;
+struct Parsecmd
+{
+	char	*name;
+	void	(*f)(char*, char*);
+};
+
+static	void	appendcmd(char*, char*);
+static	void	authenticatecmd(char*, char*);
+static	void	capabilitycmd(char*, char*);
+static	void	closecmd(char*, char*);
+static	void	copycmd(char*, char*);
+static	void	createcmd(char*, char*);
+static	void	deletecmd(char*, char*);
+static	void	expungecmd(char*, char*);
+static	void	fetchcmd(char*, char*);
+static	void	getquotacmd(char*, char*);
+static	void	getquotarootcmd(char*, char*);
+static	void	idlecmd(char*, char*);
+static	void	listcmd(char*, char*);
+static	void	logincmd(char*, char*);
+static	void	logoutcmd(char*, char*);
+static	void	namespacecmd(char*, char*);
+static	void	noopcmd(char*, char*);
+static	void	renamecmd(char*, char*);
+static	void	searchcmd(char*, char*);
+static	void	selectcmd(char*, char*);
+static	void	setquotacmd(char*, char*);
+static	void	statuscmd(char*, char*);
+static	void	storecmd(char*, char*);
+static	void	subscribecmd(char*, char*);
+static	void	uidcmd(char*, char*);
+static	void	unsubscribecmd(char*, char*);
+static	void	xdebugcmd(char*, char*);
+static	void	copyucmd(char*, char*, int);
+static	void	fetchucmd(char*, char*, int);
+static	void	searchucmd(char*, char*, int);
+static	void	storeucmd(char*, char*, int);
+
+static	void	imap4(int);
+static	void	status(int expungeable, int uids);
+static	void	cleaner(void);
+static	void	check(void);
+static	int	catcher(void*, char*);
+
+static	Search	*searchkey(int first);
+static	Search	*searchkeys(int first, Search *tail);
+static	char	*astring(void);
+static	char	*atomstring(char *disallowed, char *initial);
+static	char	*atom(void);
+static	void	clearcmd(void);
+static	char	*command(void);
+static	void	crnl(void);
+static	Fetch	*fetchatt(char *s, Fetch *f);
+static	Fetch	*fetchwhat(void);
+static	int	flaglist(void);
+static	int	flags(void);
+static	int	getc(void);
+static	char	*listmbox(void);
+static	char	*literal(void);
+static	uint	litlen(void);
+static	Msgset	*msgset(int);
+static	void	mustbe(int c);
+static	uint	number(int nonzero);
+static	int	peekc(void);
+static	char	*quoted(void);
+static	void	secttext(Fetch *, int);
+static	uint	seqno(void);
+static	Store	*storewhat(void);
+static	char	*tag(void);
+static	uint	uidno(void);
+static	void	ungetc(void);
+
+static	Parsecmd	Snonauthed[] =
+{
+	{"capability",		capabilitycmd},
+	{"logout",		logoutcmd},
+	{"noop",		noopcmd},
+	{"x-exit",		logoutcmd},
+
+	{"authenticate",	authenticatecmd},
+	{"login",		logincmd},
+
+	nil
+};
+
+static	Parsecmd	Sauthed[] =
+{
+	{"capability",		capabilitycmd},
+	{"logout",		logoutcmd},
+	{"noop",		noopcmd},
+	{"x-exit",		logoutcmd},
+	{"xdebug",		xdebugcmd},
+
+	{"append",		appendcmd},
+	{"create",		createcmd},
+	{"delete",		deletecmd},
+	{"examine",		selectcmd},
+	{"select",		selectcmd},
+	{"idle",		idlecmd},
+	{"list",		listcmd},
+	{"lsub",		listcmd},
+	{"namespace",		namespacecmd},
+	{"rename",		renamecmd},
+	{"setquota",		setquotacmd},
+	{"getquota",		getquotacmd},
+	{"getquotaroot",		getquotarootcmd},
+	{"status",		statuscmd},
+	{"subscribe",		subscribecmd},
+	{"unsubscribe",		unsubscribecmd},
+
+	nil
+};
+
+static	Parsecmd	Sselected[] =
+{
+	{"capability",		capabilitycmd},
+	{"xdebug",		xdebugcmd},
+	{"logout",		logoutcmd},
+	{"x-exit",		logoutcmd},
+	{"noop",		noopcmd},
+
+	{"append",		appendcmd},
+	{"create",		createcmd},
+	{"delete",		deletecmd},
+	{"examine",		selectcmd},
+	{"select",		selectcmd},
+	{"idle",		idlecmd},
+	{"list",		listcmd},
+	{"lsub",		listcmd},
+	{"namespace",		namespacecmd},
+	{"rename",		renamecmd},
+	{"status",		statuscmd},
+	{"subscribe",		subscribecmd},
+	{"unsubscribe",		unsubscribecmd},
+
+	{"check",		noopcmd},
+	{"close",		closecmd},
+	{"copy",		copycmd},
+	{"expunge",		expungecmd},
+	{"fetch",		fetchcmd},
+	{"search",		searchcmd},
+	{"store",		storecmd},
+	{"uid",			uidcmd},
+
+	nil
+};
+
+static	char		*atomstop = "(){%*\"\\";
+static	Parsecmd	*imapstate;
+static	jmp_buf		parsejmp;
+static	char		*parsemsg;
+static	int		allowpass;
+static	int		allowcr;
+static	int		exiting;
+static	QLock		imaplock;
+static	int		idlepid = -1;
+
+Biobuf	bout;
+Biobuf	bin;
+char	username[Userlen];
+char	mboxdir[Pathlen];
+char	*servername;
+char	*site;
+char	*remote;
+char	*binupas;
+Box	*selected;
+Bin	*parsebin;
+int	debug;
+Uidplus	*uidlist;
+Uidplus	**uidtl;
+
+void
+usage(void)
+{
+	fprint(2, "usage: upas/imap4d [-acpv] [-l logfile] [-b binupas] [-d site] [-r remotehost] [-s servername]\n");
+	bye("usage");
+}
+
+void
+main(int argc, char *argv[])
+{
+	int preauth;
+
+	Binit(&bin, dup(0, -1), OREAD);
+	close(0);
+	Binit(&bout, 1, OWRITE);
+	tmfmtinstall();
+	quotefmtinstall();
+	fmtinstall('F', Ffmt);
+	fmtinstall('D', Dfmt);	/* rfc822; # imap date %Z */
+	fmtinstall(L'δ', Dfmt);	/* rfc822; # imap date %s */
+	fmtinstall('X', Xfmt);
+	fmtinstall('Y', Zfmt);
+	fmtinstall('Z', Zfmt);
+
+	/* for auth */
+	fmtinstall('H', encodefmt);
+	fmtinstall('[', encodefmt);
+
+	preauth = 0;
+	allowpass = 0;
+	allowcr = 0;
+	ARGBEGIN{
+	case 'a':
+		preauth = 1;
+		break;
+	case 'b':
+		binupas = EARGF(usage());
+		break;
+	case 'c':
+		allowcr = 1;
+		break;
+	case 'd':
+		site = EARGF(usage());
+		break;
+	case 'l':
+		snprint(logfile, sizeof logfile, "%s", EARGF(usage()));
+		break;
+	case 'p':
+		allowpass = 1;
+		break;
+	case 'r':
+		remote = EARGF(usage());
+		break;
+	case 's':
+		servername = EARGF(usage());
+		break;
+	case 'v':
+		debug ^= 1;
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND
+
+	if(allowpass && allowcr){
+		fprint(2, "imap4d: -c and -p are mutually exclusive\n");
+		usage();
+	}
+
+	if(preauth)
+		setupuser(nil);
+
+	if(servername == nil){
+		servername = csquery("sys", sysname(), "dom");
+		if(servername == nil)
+			servername = sysname();
+		if(servername == nil){
+			fprint(2, "ip/imap4d can't find server name: %r\n");
+			bye("can't find system name");
+		}
+	}
+	if(site == nil)
+		site = getenv("site");
+	if(site == nil){
+		site = strchr(servername, '.');
+		if(site)
+			site++;
+		else
+			site = servername;
+	}
+
+	rfork(RFNOTEG|RFREND);
+
+	atnotify(catcher, 1);
+	qlock(&imaplock);
+	atexit(cleaner);
+	imap4(preauth);
+}
+
+static void
+imap4(int preauth)
+{
+	char *volatile tg;
+	char *volatile cmd;
+	Parsecmd *st;
+
+	if(preauth){
+		Bprint(&bout, "* preauth %s IMAP4rev1 server ready user %s authenticated\r\n", servername, username);
+		imapstate = Sauthed;
+	}else{
+		Bprint(&bout, "* OK %s IMAP4rev1 server ready\r\n", servername);
+		imapstate = Snonauthed;
+	}
+	if(Bflush(&bout) < 0)
+		writeerr();
+
+	tg = nil;
+	cmd = nil;
+	if(setjmp(parsejmp)){
+		if(tg == nil)
+			Bprint(&bout, "* bad empty command line: %s\r\n", parsemsg);
+		else if(cmd == nil)
+			Bprint(&bout, "%s BAD no command: %s\r\n", tg, parsemsg);
+		else
+			Bprint(&bout, "%s BAD %s %s\r\n", tg, cmd, parsemsg);
+		clearcmd();
+		if(Bflush(&bout) < 0)
+			writeerr();
+		binfree(&parsebin);
+	}
+	for(;;){
+		if(mblocked())
+			bye("internal error: mailbox lock held");
+		tg = nil;
+		cmd = nil;
+		tg = tag();
+		mustbe(' ');
+		cmd = atom();
+
+		/*
+		 * note: outlook express is broken: it requires echoing the
+		 * command as part of matching response
+		 */
+		for(st = imapstate; st->name != nil; st++)
+			if(cistrcmp(cmd, st->name) == 0){
+				st->f(tg, cmd);
+				break;
+			}
+		if(st->name == nil){
+			clearcmd();
+			Bprint(&bout, "%s BAD %s illegal command\r\n", tg, cmd);
+		}
+
+		if(Bflush(&bout) < 0)
+			writeerr();
+		binfree(&parsebin);
+	}
+}
+
+void
+bye(char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	Bprint(&bout, "* bye ");
+	Bvprint(&bout, fmt, arg);
+	Bprint(&bout, "\r\n");
+	Bflush(&bout);
+	exits(0);
+}
+
+void
+parseerr(char *msg)
+{
+	debuglog("parse error: %s", msg);
+	parsemsg = msg;
+	longjmp(parsejmp, 1);
+}
+
+/*
+ * an error occured while writing to the client
+ */
+void
+writeerr(void)
+{
+	cleaner();
+	_exits("connection closed");
+}
+
+static int
+catcher(void *, char *msg)
+{
+	if(strstr(msg, "closed pipe") != nil)
+		return 1;
+	return 0;
+}
+
+/*
+ * wipes out the idlecmd backgroung process if it is around.
+ * this can only be called if the current proc has qlocked imaplock.
+ * it must be the last piece of imap4d code executed.
+ */
+static void
+cleaner(void)
+{
+	int i;
+
+	debuglog("cleaner");
+	if(idlepid < 0)
+		return;
+	exiting = 1;
+	close(0);
+	close(1);
+	close(2);
+	close(bin.fid);
+	bin.fid = -1;
+	/*
+	 * the other proc is either stuck in a read, a sleep,
+	 * or is trying to lock imap4lock.
+	 * get him out of it so he can exit cleanly
+	 */
+	qunlock(&imaplock);
+	for(i = 0; i < 4; i++)
+		postnote(PNGROUP, getpid(), "die");
+}
+
+/*
+ * send any pending status updates to the client
+ * careful: shouldn't exit, because called by idle polling proc
+ *
+ * can't always send pending info
+ * in particular, can't send expunge info
+ * in response to a fetch, store, or search command.
+ * 
+ * rfc2060 5.2:	server must send mailbox size updates
+ * rfc2060 5.2:	server may send flag updates
+ * rfc2060 5.5:	servers prohibited from sending expunge while fetch, store, search in progress
+ * rfc2060 7:	in selected state, server checks mailbox for new messages as part of every command
+ * 		sends untagged EXISTS and RECENT respsonses reflecting new size of the mailbox
+ * 		should also send appropriate untagged FETCH and EXPUNGE messages if another agent
+ * 		changes the state of any message flags or expunges any messages
+ * rfc2060 7.4.1	expunge server response must not be sent when no command is in progress,
+ * 		nor while responding to a fetch, stort, or search command (uid versions are ok)
+ * 		command only "in progress" after entirely parsed.
+ *
+ * strategy for third party deletion of messages or of a mailbox
+ *
+ * deletion of a selected mailbox => act like all message are expunged
+ *	not strictly allowed by rfc2180, but close to method 3.2.
+ *
+ * renaming same as deletion
+ *
+ * copy
+ *	reject iff a deleted message is in the request
+ *
+ * search, store, fetch operations on expunged messages
+ *	ignore the expunged messages
+ *	return tagged no if referenced
+ */
+static void
+status(int expungeable, int uids)
+{
+	int tell;
+
+	if(!selected)
+		return;
+	tell = 0;
+	if(expungeable)
+		tell = expungemsgs(selected, 1);
+	if(selected->sendflags)
+		sendflags(selected, uids);
+	if(tell || selected->toldmax != selected->max){
+		Bprint(&bout, "* %ud EXISTS\r\n", selected->max);
+		selected->toldmax = selected->max;
+	}
+	if(tell || selected->toldrecent != selected->recent){
+		Bprint(&bout, "* %ud RECENT\r\n", selected->recent);
+		selected->toldrecent = selected->recent;
+	}
+	if(tell)
+		closeimp(selected, checkbox(selected, 1));
+}
+
+/*
+ * careful: can't exit, because called by idle polling proc
+ */
+static void
+check(void)
+{
+	if(!selected)
+		return;
+	checkbox(selected, 0);
+	status(1, 0);
+}
+
+static void
+appendcmd(char *tg, char *cmd)
+{
+	char *mbox, head[128];
+	uint t, n, now;
+	int flags, ok;
+	Tzone *tz;
+	Tm tm;
+	Uidplus u;
+
+	mustbe(' ');
+	mbox = astring();
+	mustbe(' ');
+	flags = 0;
+	if(peekc() == '('){
+		flags = flaglist();
+		mustbe(' ');
+	}
+	now = time(nil);
+	if(peekc() == '"'){
+		t = imap4datetime(quoted());
+		if(t == ~0)
+			parseerr("illegal date format");
+		mustbe(' ');
+		if(t > now)
+			t = now;
+	}else
+		t = now;
+	n = litlen();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		check();
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+	/* bug.  this is upas/fs's job */
+	if(!cdexists(mboxdir, mbox)){
+		check();
+		Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd);
+		return;
+	}
+
+	tz = tzload("local");
+	tmtime(&tm, t, tz);
+	snprint(head, sizeof head, "From %s %τ", username, tmfmt(&tm, "WW MMM _D hh:mm:ss Z YYYY"));
+	ok = appendsave(mbox, flags, head, &bin, n, &u);
+	crnl();
+	check();
+	if(ok)
+		Bprint(&bout, "%s OK [APPENDUID %ud %ud] %s completed\r\n",
+			tg, u.uidvalidity, u.uid, cmd);
+	else
+		Bprint(&bout, "%s NO %s message save failed\r\n", tg, cmd);
+}
+
+static void
+authenticatecmd(char *tg, char *cmd)
+{
+	char *s, *t;
+
+	mustbe(' ');
+	s = atom();
+	if(cistrcmp(s, "cram-md5") == 0){
+		crnl();
+		t = cramauth();
+		if(t == nil){
+			Bprint(&bout, "%s OK %s\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}else
+			Bprint(&bout, "%s NO %s failed %s\r\n", tg, cmd, t);
+	}else if(cistrcmp(s, "plain") == 0){
+		s = nil;
+		if(peekc() == ' '){
+			mustbe(' ');
+			s = astring();
+		}
+		crnl();
+		if(!allowpass)
+			Bprint(&bout, "%s NO %s plaintext passwords disallowed\r\n", tg, cmd);
+		else if(t = plainauth(s))
+			Bprint(&bout, "%s NO %s failed %s\r\n", tg, cmd, t);
+		else{
+			Bprint(&bout, "%s OK %s\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}
+	}else
+		Bprint(&bout, "%s NO %s unsupported authentication protocol\r\n", tg, cmd);
+}
+
+static void
+capabilitycmd(char *tg, char *cmd)
+{
+	crnl();
+	check();
+	Bprint(&bout, "* CAPABILITY IMAP4REV1 IDLE NAMESPACE QUOTA XDEBUG");
+	Bprint(&bout, " UIDPLUS");
+	if(allowpass || allowcr)
+		Bprint(&bout, " AUTH=CRAM-MD5 AUTH=PLAIN");
+	else
+		Bprint(&bout, " LOGINDISABLED AUTH=CRAM-MD5");
+	Bprint(&bout, "\r\n%s OK %s\r\n", tg, cmd);
+}
+
+static void
+closecmd(char *tg, char *cmd)
+{
+	crnl();
+	imapstate = Sauthed;
+	closebox(selected, 1);
+	selected = nil;
+	Bprint(&bout, "%s OK %s mailbox closed, now in authenticated state\r\n", tg, cmd);
+}
+
+/*
+ * note: message id's are before any pending expunges
+ */
+static void
+copycmd(char *tg, char *cmd)
+{
+	copyucmd(tg, cmd, 0);
+}
+
+static char *uidpsep;
+static int
+printuid(Box*, Msg *m, int, void*)
+{
+	Bprint(&bout, "%s%ud", uidpsep, m->uid);
+	uidpsep = ",";
+	return 1;
+}
+
+static void
+copyucmd(char *tg, char *cmd, int uids)
+{
+	char *uid, *mbox;
+	int ok;
+	uint max;
+	Msgset *ms;
+	Uidplus *u;
+
+	mustbe(' ');
+	ms = msgset(uids);
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+
+	uid = "";
+	if(uids)
+		uid = "UID ";
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		status(1, uids);
+		Bprint(&bout, "%s NO %s%s bad mailbox\r\n", tg, uid, cmd);
+		return;
+	}
+	if(!cdexists(mboxdir, mbox)){
+		check();
+		Bprint(&bout, "%s NO [TRYCREATE] %s mailbox does not exist\r\n", tg, cmd);
+		return;
+	}
+
+	uidlist = 0;
+	uidtl = &uidlist;
+
+	max = selected->max;
+	checkbox(selected, 0);
+	ok = formsgs(selected, ms, max, uids, copycheck, nil);
+	if(ok)
+		ok = formsgs(selected, ms, max, uids, copysaveu, mbox);
+	status(1, uids);
+	if(ok && uidlist){
+		u = uidlist;
+		Bprint(&bout, "%s OK [COPYUID %ud", tg, u->uidvalidity);
+		uidpsep = " ";
+		formsgs(selected, ms, max, uids, printuid, mbox);
+		Bprint(&bout, " %ud", u->uid);
+		for(u = u->next; u; u = u->next)
+			Bprint(&bout, ",%ud", u->uid);
+		Bprint(&bout, "] %s%s completed\r\n", uid, cmd);
+	}else if(ok)
+		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+	else
+		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
+}
+
+static void
+createcmd(char *tg, char *cmd)
+{
+	char *mbox;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+	if(cistrcmp(mbox, "mbox") == 0){
+		Bprint(&bout, "%s NO %s cannot remotely create INBOX\r\n", tg, cmd);
+		return;
+	}
+	if(creatembox(mbox) == -1)
+		Bprint(&bout, "%s NO %s cannot create mailbox %#Y\r\n", tg, cmd, mbox);
+	else
+		Bprint(&bout, "%s OK %#Y %s completed\r\n", tg, mbox, cmd);
+}
+
+static void
+xdebugcmd(char *tg, char *)
+{
+	char *s, *t;
+
+	mustbe(' ');
+	s = astring();
+	t = 0;
+	if(!cistrcmp(s, "file")){
+		mustbe(' ');
+		t = astring();
+	}
+	crnl();
+	check();
+	if(!cistrcmp(s, "on") || !cistrcmp(s, "1")){
+		Bprint(&bout, "%s OK debug on\r\n", tg);
+		debug = 1;
+	}else if(!cistrcmp(s, "file")){
+		if(!strstr(t, ".."))
+			snprint(logfile, sizeof logfile, "%s", t);
+		Bprint(&bout, "%s OK debug file %#Z\r\n", tg, logfile);
+	}else{
+		Bprint(&bout, "%s OK debug off\r\n", tg);
+		debug = 0;
+	}
+}
+
+static void
+deletecmd(char *tg, char *cmd)
+{
+	char *mbox;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+
+	/*
+	 * i don't know if this is a hack or not.  a delete of the
+	 * currently-selected box seems fishy.  the standard doesn't
+	 * specify any behavior.
+	 */
+	if(selected && strcmp(selected->name, mbox) == 0){
+		ilog("delete: client bug? close of selected mbox %s", selected->fs);
+		imapstate = Sauthed;
+		closebox(selected, 1);
+		selected = nil;
+		setname("[none]");
+	}
+
+	if(!cistrcmp(mbox, "mbox") || !removembox(mbox) == -1)
+		Bprint(&bout, "%s NO %s cannot delete mailbox %#Y\r\n", tg, cmd, mbox);
+	else
+		Bprint(&bout, "%s OK %#Y %s completed\r\n", tg, mbox, cmd);
+}
+
+static void
+expungeucmd(char *tg, char *cmd, int uids)
+{
+	int ok;
+	Msgset *ms;
+
+	ms = 0;
+	if(uids){
+		mustbe(' ');
+		ms = msgset(uids);
+	}
+	crnl();
+	ok = deletemsg(selected, ms);
+	check();
+	if(ok)
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s NO %s some messages not expunged\r\n", tg, cmd);
+}
+
+static void
+expungecmd(char *tg, char *cmd)
+{
+	expungeucmd(tg, cmd, 0);
+}
+
+static void
+fetchcmd(char *tg, char *cmd)
+{
+	fetchucmd(tg, cmd, 0);
+}
+
+static void
+fetchucmd(char *tg, char *cmd, int uids)
+{
+	char *uid;
+	int ok;
+	uint max;
+	Fetch *f;
+	Msgset *ms;
+	Mblock *ml;
+
+	mustbe(' ');
+	ms = msgset(uids);
+	mustbe(' ');
+	f = fetchwhat();
+	crnl();
+	uid = "";
+	if(uids)
+		uid = "uid ";
+	max = selected->max;
+	ml = checkbox(selected, 1);
+	if(ml != nil)
+		formsgs(selected, ms, max, uids, fetchseen, f);
+	closeimp(selected, ml);
+	ok = ml != nil && formsgs(selected, ms, max, uids, fetchmsg, f);
+	status(uids, uids);
+	if(ok)
+		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+	else{
+		if(ml == nil)
+			ilog("nil maillock\n");
+		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
+	}
+}
+
+static void
+idlecmd(char *tg, char *cmd)
+{
+	int c, pid;
+
+	crnl();
+	Bprint(&bout, "+ idling, waiting for done\r\n");
+	if(Bflush(&bout) < 0)
+		writeerr();
+
+	if(idlepid < 0){
+		pid = rfork(RFPROC|RFMEM|RFNOWAIT);
+		if(pid == 0){
+			setname("imap idle");
+			for(;;){
+				qlock(&imaplock);
+				if(exiting)
+					break;
+
+				/*
+				 * parent may have changed curdir, but it doesn't change our .
+				 */
+				resetcurdir();
+
+				check();
+				if(Bflush(&bout) < 0)
+					writeerr();
+				qunlock(&imaplock);
+				sleep(15*1000);
+				enableforwarding();
+			}
+			_exits(0);
+		}
+		idlepid = pid;
+	}
+
+	qunlock(&imaplock);
+
+	/*
+	 * clear out the next line, which is supposed to contain (case-insensitive)
+	 * done\n
+	 * this is special code since it has to dance with the idle polling proc
+	 * and handle exiting correctly.
+	 */
+	for(;;){
+		c = getc();
+		if(c < 0){
+			qlock(&imaplock);
+			if(!exiting)
+				cleaner();
+			_exits(0);
+		}
+		if(c == '\n')
+			break;
+	}
+
+	qlock(&imaplock);
+	if(exiting)
+		_exits(0);
+
+	/*
+	 * child may have changed curdir, but it doesn't change our .
+	 */
+	resetcurdir();
+	check();
+	Bprint(&bout, "%s OK %s terminated\r\n", tg, cmd);
+}
+
+static void
+listcmd(char *tg, char *cmd)
+{
+	char *s, *t, *ref, *mbox;
+
+	mustbe(' ');
+	s = astring();
+	mustbe(' ');
+	t = listmbox();
+	crnl();
+	check();
+	ref = mutf7str(s);
+	mbox = mutf7str(t);
+	if(ref == nil || mbox == nil){
+		Bprint(&bout, "%s BAD %s modified utf-7\r\n", tg, cmd);
+		return;
+	}
+
+	/*
+	 * special request for hierarchy delimiter and root name
+	 * root name appears to be name up to and including any delimiter,
+	 * or the empty string, if there is no delimiter.
+	 *
+	 * this must change if the # namespace convention is supported.
+	 */
+	if(*mbox == '\0'){
+		s = strchr(ref, '/');
+		if(s == nil)
+			ref = "";
+		else
+			s[1] = '\0';
+		Bprint(&bout, "* %s (\\Noselect) \"/\" \"%s\"\r\n", cmd, ref);
+		Bprint(&bout, "%s OK %s\r\n", tg, cmd);
+		return;
+	}
+
+	/*
+	 * hairy exception: these take non-fsencoded strings.  BUG?
+	 */
+	if(cistrcmp(cmd, "lsub") == 0)
+		lsubboxes(cmd, ref, mbox);
+	else
+		listboxes(cmd, ref, mbox);
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+logincmd(char *tg, char *cmd)
+{
+	char *r, *s, *t;
+
+	mustbe(' ');
+	s = astring();	/* uid */
+	mustbe(' ');
+	t = astring();	/* password */
+	crnl();
+	if(allowcr){
+		if(r = crauth(s, t)){
+			Bprint(&bout, "* NO [ALERT] %s\r\n", r);
+			Bprint(&bout, "%s NO %s succeeded\r\n", tg, cmd);
+		}else{
+			Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}
+		return;
+	}else if(allowpass){
+		if(r = passauth(s, t))
+			Bprint(&bout, "%s NO %s failed check [%s]\r\n", tg, cmd, r);
+		else{
+			Bprint(&bout, "%s OK %s succeeded\r\n", tg, cmd);
+			imapstate = Sauthed;
+		}
+		return;
+	}
+	Bprint(&bout, "%s NO %s plaintext passwords disallowed\r\n", tg, cmd);
+}
+
+/*
+ * logout or x-exit, which doesn't expunge the mailbox
+ */
+static void
+logoutcmd(char *tg, char *cmd)
+{
+	crnl();
+
+	if(cmd[0] != 'x' && selected){
+		closebox(selected, 1);
+		selected = nil;
+	}
+	Bprint(&bout, "* bye\r\n");
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	exits(0);
+}
+
+static void
+namespacecmd(char *tg, char *cmd)
+{
+	crnl();
+	check();
+
+	/*
+	 * personal, other users, shared namespaces
+	 * send back nil or descriptions of (prefix heirarchy-delim) for each case
+	 */
+	Bprint(&bout, "* NAMESPACE ((\"\" \"/\")) nil nil\r\n");
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+noopcmd(char *tg, char *cmd)
+{
+	crnl();
+	check();
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	enableforwarding();
+}
+
+static void
+getquota0(char *tg, char *cmd, char *r)
+{
+extern vlong getquota(void);
+	vlong v;
+
+	if(r[0]){
+		Bprint(&bout, "%s NO %s no such quota root\r\n", tg, cmd);
+		return;
+	}
+	v = getquota();
+	if(v == -1){
+		Bprint(&bout, "%s NO %s bad [%r]\r\n", tg, cmd);
+		return;
+	}
+	Bprint(&bout, "* %s "" (storage %llud %d)\r\n", cmd, v/1024, 256*1024);
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+getquotacmd(char *tg, char *cmd)
+{
+	char *r;
+
+	mustbe(' ');
+	r = astring();
+	crnl();
+	check();
+	getquota0(tg, cmd, r);
+}
+
+static void
+getquotarootcmd(char *tg, char *cmd)
+{
+	char *r;
+
+	mustbe(' ');
+	r = astring();
+	crnl();
+	check();
+
+	Bprint(&bout, "* %s %s \"\"\r\n", cmd, r);
+	getquota0(tg, cmd, "");
+}
+
+static void
+setquotacmd(char *tg, char *cmd)
+{
+	mustbe(' ');
+	astring();
+	mustbe(' ');
+	mustbe('(');
+	for(;;){
+		astring();
+		mustbe(' ');
+		number(0);
+		if(peekc() == ')')
+			break;
+	}
+	getc();
+	crnl();
+	check();
+	Bprint(&bout, "%s NO %s error: can't set that data\r\n", tg, cmd);
+}
+
+/*
+ * this is only a partial implementation
+ * should copy files to other directories,
+ * and copy & truncate inbox
+ */
+static void
+renamecmd(char *tg, char *cmd)
+{
+	char *from, *to;
+
+	mustbe(' ');
+	from = astring();
+	mustbe(' ');
+	to = astring();
+	crnl();
+	check();
+
+	to = mboxname(to);
+	if(to == nil || cistrcmp(to, "mbox") == 0){
+		Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd);
+		return;
+	}
+	if(access(to, AEXIST) >= 0){
+		Bprint(&bout, "%s NO %s mailbox already exists\r\n", tg, cmd);
+		return;
+	}
+	from = mboxname(from);
+	if(from == nil){
+		Bprint(&bout, "%s NO %s bad mailbox destination name\r\n", tg, cmd);
+		return;
+	}
+	if(renamebox(from, to, strcmp(from, "mbox")))
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s NO %s failed\r\n", tg, cmd);
+}
+
+static void
+searchcmd(char *tg, char *cmd)
+{
+	searchucmd(tg, cmd, 0);
+}
+
+/*
+ * mail.app has a vicious habit of appending a message to
+ * a folder and then immediately searching for it by message-id.
+ * for a 10,000 message sent folder, this can be quite painful.
+ *
+ * evil strategy.  for message-id searches, check the last
+ * message in the mailbox!  if that fails, use the normal algorithm.
+ */
+static Msg*
+mailappsucks(Search *s)
+{
+	Msg *m;
+
+	if(s->key == SKuid)
+		s = s->next;
+	if(s && s->next == nil)
+	if(s->key == SKheader && cistrcmp(s->hdr, "message-id") == 0){
+		for(m = selected->msgs; m && m->next; m = m->next)
+			;
+		if(m != nil)
+		if(m->matched = searchmsg(m, s, 0))
+			return m;
+	}
+	return 0;
+}
+
+static void
+searchucmd(char *tg, char *cmd, int uids)
+{
+	char *uid;
+	uint id, ld;
+	Msg *m;
+	Search rock;
+
+	mustbe(' ');
+	rock.next = nil;
+	searchkeys(1, &rock);
+	crnl();
+	uid = "";
+	if(uids)
+		uid = "UID ";
+	if(rock.next != nil && rock.next->key == SKcharset){
+		if(cistrcmp(rock.next->s, "utf-8") != 0
+		&& cistrcmp(rock.next->s, "us-ascii") != 0){
+			Bprint(&bout, "%s NO [BADCHARSET] (\"US-ASCII\" \"UTF-8\") %s%s failed\r\n", tg, uid, cmd);
+			checkbox(selected, 0);
+			status(uids, uids);
+			return;
+		}
+		rock.next = rock.next->next;
+	}
+	Bprint(&bout, "* SEARCH");
+	if(m = mailappsucks(rock.next))
+			goto cheat;
+	ld = searchld(rock.next);
+	for(m = selected->msgs; m != nil; m = m->next)
+		m->matched = searchmsg(m, rock.next, ld);
+	for(m = selected->msgs; m != nil; m = m->next){
+cheat:
+		if(m->matched){
+			if(uids)
+				id = m->uid;
+			else
+				id = m->seq;
+			Bprint(&bout, " %ud", id);
+		}
+	}
+	Bprint(&bout, "\r\n");
+	checkbox(selected, 0);
+	status(uids, uids);
+	Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+}
+
+static void
+selectcmd(char *tg, char *cmd)
+{
+	char *s, *m0, *mbox, buf[Pathlen];
+	Msg *m;
+
+	mustbe(' ');
+	m0 = astring();
+	crnl();
+
+	if(selected){
+		imapstate = Sauthed;
+		closebox(selected, 1);
+		selected = nil;
+		setname("[none]");
+	}
+	debuglog("select %s", m0);
+
+	mbox = mboxname(m0);
+	if(mbox == nil){
+		debuglog("select %s [%s] -> no bad", mbox, m0);
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+
+	selected = openbox(mbox, "imap", cistrcmp(cmd, "select") == 0);
+	if(selected == nil){
+		Bprint(&bout, "%s NO %s can't open mailbox %#Y: %r\r\n", tg, cmd, mbox);
+		return;
+	}
+
+	setname("%s", decfs(buf, sizeof buf, selected->name));
+	imapstate = Sselected;
+
+	Bprint(&bout, "* FLAGS (\\Seen \\Answered \\Flagged \\Deleted \\Draft)\r\n");
+	Bprint(&bout, "* %ud EXISTS\r\n", selected->max);
+	selected->toldmax = selected->max;
+	Bprint(&bout, "* %ud RECENT\r\n", selected->recent);
+	selected->toldrecent = selected->recent;
+	for(m = selected->msgs; m != nil; m = m->next){
+		if(!m->expunged && (m->flags & Fseen) != Fseen){
+			Bprint(&bout, "* OK [UNSEEN %ud]\r\n", m->seq);
+			break;
+		}
+	}
+	Bprint(&bout, "* OK [PERMANENTFLAGS (\\Seen \\Answered \\Flagged \\Draft \\Deleted)]\r\n");
+	Bprint(&bout, "* OK [UIDNEXT %ud]\r\n", selected->uidnext);
+	Bprint(&bout, "* OK [UIDVALIDITY %ud]\r\n", selected->uidvalidity);
+	s = "READ-ONLY";
+	if(selected->writable)
+		s = "READ-WRITE";
+	Bprint(&bout, "%s OK [%s] %s %#Y completed\r\n", tg, s, cmd, mbox);
+}
+
+static Namedint	statusitems[] =
+{
+	{"MESSAGES",	Smessages},
+	{"RECENT",	Srecent},
+	{"UIDNEXT",	Suidnext},
+	{"UIDVALIDITY",	Suidvalidity},
+	{"UNSEEN",	Sunseen},
+	{nil,		0}
+};
+
+static void
+statuscmd(char *tg, char *cmd)
+{
+	char *s, *mbox;
+	int si, i, opened;
+	uint v;
+	Box *box;
+	Msg *m;
+
+	mustbe(' ');
+	mbox = astring();
+	mustbe(' ');
+	mustbe('(');
+	si = 0;
+	for(;;){
+		s = atom();
+		i = mapint(statusitems, s);
+		if(i == 0)
+			parseerr("illegal status item");
+		si |= i;
+		if(peekc() == ')')
+			break;
+		mustbe(' ');
+	}
+	mustbe(')');
+	crnl();
+
+	mbox = mboxname(mbox);
+	if(mbox == nil){
+		check();
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+		return;
+	}
+
+	opened = 0;
+	if(selected && !strcmp(mbox, selected->name))
+		box = selected;
+	else{
+		box = openbox(mbox, "status", 1);
+		if(box == nil){
+			check();
+			Bprint(&bout, "%s NO [TRYCREATE] %s can't open mailbox %#Y: %r\r\n", tg, cmd, mbox);
+			return;
+		}
+		opened = 1;
+	}
+
+	Bprint(&bout, "* STATUS %#Y (", mbox);
+	s = "";
+	for(i = 0; statusitems[i].name != nil; i++)
+		if(si & statusitems[i].v){
+			v = 0;
+			switch(statusitems[i].v){
+			case Smessages:
+				v = box->max;
+				break;
+			case Srecent:
+				v = box->recent;
+				break;
+			case Suidnext:
+				v = box->uidnext;
+				break;
+			case Suidvalidity:
+				v = box->uidvalidity;
+				break;
+			case Sunseen:
+				v = 0;
+				for(m = box->msgs; m != nil; m = m->next)
+					if((m->flags & Fseen) != Fseen)
+						v++;
+				break;
+			default:
+				Bprint(&bout, ")");
+				bye("internal error: status item not implemented");
+				break;
+			}
+			Bprint(&bout, "%s%s %ud", s, statusitems[i].name, v);
+			s = " ";
+		}
+	Bprint(&bout, ")\r\n");
+	if(opened)
+		closebox(box, 1);
+
+	check();
+	Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+storecmd(char *tg, char *cmd)
+{
+	storeucmd(tg, cmd, 0);
+}
+
+static void
+storeucmd(char *tg, char *cmd, int uids)
+{
+	char *uid;
+	int ok;
+	uint max;
+	Mblock *ml;
+	Msgset *ms;
+	Store *st;
+
+	mustbe(' ');
+	ms = msgset(uids);
+	mustbe(' ');
+	st = storewhat();
+	crnl();
+	uid = "";
+	if(uids)
+		uid = "uid ";
+	max = selected->max;
+	ml = checkbox(selected, 1);
+	ok = ml != nil && formsgs(selected, ms, max, uids, storemsg, st);
+	closeimp(selected, ml);
+	status(uids, uids);
+	if(ok)
+		Bprint(&bout, "%s OK %s%s completed\r\n", tg, uid, cmd);
+	else
+		Bprint(&bout, "%s NO %s%s failed\r\n", tg, uid, cmd);
+}
+
+/*
+ * minimal implementation of subscribe
+ * all folders are automatically subscribed,
+ * and can't be unsubscribed
+ */
+static void
+subscribecmd(char *tg, char *cmd)
+{
+	char *mbox;
+	int ok;
+	Box *box;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+	mbox = mboxname(mbox);
+	ok = 0;
+	if(mbox != nil && (box = openbox(mbox, "subscribe", 0))){
+		ok = subscribe(mbox, 's');
+		closebox(box, 1);
+	}
+	if(!ok)
+		Bprint(&bout, "%s NO %s bad mailbox\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static void
+uidcmd(char *tg, char *cmd)
+{
+	char *sub;
+
+	mustbe(' ');
+	sub = atom();
+	if(cistrcmp(sub, "copy") == 0)
+		copyucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "fetch") == 0)
+		fetchucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "search") == 0)
+		searchucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "store") == 0)
+		storeucmd(tg, sub, 1);
+	else if(cistrcmp(sub, "expunge") == 0)
+		expungeucmd(tg, sub, 1);
+	else{
+		clearcmd();
+		Bprint(&bout, "%s BAD %s illegal uid command %s\r\n", tg, cmd, sub);
+	}
+}
+
+static void
+unsubscribecmd(char *tg, char *cmd)
+{
+	char *mbox;
+
+	mustbe(' ');
+	mbox = astring();
+	crnl();
+	check();
+	mbox = mboxname(mbox);
+	if(mbox == nil || !subscribe(mbox, 'u'))
+		Bprint(&bout, "%s NO %s can't unsubscribe\r\n", tg, cmd);
+	else
+		Bprint(&bout, "%s OK %s completed\r\n", tg, cmd);
+}
+
+static char *gbuf;
+static void
+badsyn(void)
+{
+	debuglog("syntax error [%s]", gbuf);
+	parseerr("bad syntax");
+}
+
+static void
+clearcmd(void)
+{
+	int c;
+
+	for(;;){
+		c = getc();
+		if(c < 0)
+			bye("end of input");
+		if(c == '\n')
+			return;
+	}
+}
+
+static void
+crnl(void)
+{
+	int c;
+
+	c = getc();
+	if(c == '\n')
+		return;
+	if(c != '\r' || getc() != '\n')
+		badsyn();
+}
+
+static void
+mustbe(int c)
+{
+	int x;
+
+	if((x = getc()) != c){
+		ungetc();
+		ilog("must be '%c' got %c", c, x);
+		badsyn();
+	}
+}
+
+/*
+ * flaglist	: '(' ')' | '(' flags ')'
+ */
+static int
+flaglist(void)
+{
+	int f;
+
+	mustbe('(');
+	f = 0;
+	if(peekc() != ')')
+		f = flags();
+
+	mustbe(')');
+	return f;
+}
+
+/*
+ * flags	: flag | flags ' ' flag
+ * flag		: '\' atom | atom
+ */
+static int
+flags(void)
+{
+	char *s;
+	int ff, flags, c;
+
+	flags = 0;
+	for(;;){
+		c = peekc();
+		if(c == '\\'){
+			mustbe('\\');
+			s = atomstring(atomstop, "\\");
+		}else if(strchr(atomstop, c) != nil)
+			s = atom();
+		else
+			break;
+		ff = mapflag(s);
+		if(ff == 0)
+			parseerr("flag not supported");
+		flags |= ff;
+		if(peekc() != ' ')
+			break;
+		mustbe(' ');
+	}
+	if(flags == 0)
+		parseerr("no flags given");
+	return flags;
+}
+
+/*
+ * storewhat	: osign 'FLAGS' ' ' storeflags
+ *		| osign 'FLAGS.SILENT' ' ' storeflags
+ * osign	:
+ *		| '+' | '-'
+ * storeflags	: flaglist | flags
+ */
+static Store*
+storewhat(void)
+{
+	char *s;
+	int c, f, w;
+
+	c = peekc();
+	if(c == '+' || c == '-')
+		mustbe(c);
+	else
+		c = 0;
+	s = atom();
+	w = 0;
+	if(cistrcmp(s, "flags") == 0)
+		w = Stflags;
+	else if(cistrcmp(s, "flags.silent") == 0)
+		w = Stflagssilent;
+	else
+		parseerr("illegal store attribute");
+	mustbe(' ');
+	if(peekc() == '(')
+		f = flaglist();
+	else
+		f = flags();
+	return mkstore(c, w, f);
+}
+
+/*
+ * fetchwhat	: "ALL" | "FULL" | "FAST" | fetchatt | '(' fetchatts ')'
+ * fetchatts	: fetchatt | fetchatts ' ' fetchatt
+ */
+static char *fetchatom	= "(){}%*\"\\[]";
+static Fetch*
+fetchwhat(void)
+{
+	char *s;
+	Fetch *f;
+
+	if(peekc() == '('){
+		getc();
+		f = nil;
+		for(;;){
+			s = atomstring(fetchatom, "");
+			f = fetchatt(s, f);
+			if(peekc() == ')')
+				break;
+			mustbe(' ');
+		}
+		getc();
+		return revfetch(f);
+	}
+
+	s = atomstring(fetchatom, "");
+	if(cistrcmp(s, "all") == 0)
+		f = mkfetch(Fflags, mkfetch(Finternaldate, mkfetch(Frfc822size, mkfetch(Fenvelope, nil))));
+	else if(cistrcmp(s, "fast") == 0)
+		f = mkfetch(Fflags, mkfetch(Finternaldate, mkfetch(Frfc822size, nil)));
+	else if(cistrcmp(s, "full") == 0)
+		f = mkfetch(Fflags, mkfetch(Finternaldate, mkfetch(Frfc822size, mkfetch(Fenvelope, mkfetch(Fbody, nil)))));
+	else
+		f = fetchatt(s, nil);
+	return f;
+}
+
+/*
+ * fetchatt	: "ENVELOPE" | "FLAGS" | "INTERNALDATE"
+ *		| "RFC822" | "RFC822.HEADER" | "RFC822.SIZE" | "RFC822.TEXT"
+ *		| "BODYSTRUCTURE"
+ *		| "UID"
+ *		| "BODY"
+ *		| "BODY" bodysubs
+ *		| "BODY.PEEK" bodysubs
+ * bodysubs	: sect
+ *		| sect '<' number '.' nz-number '>'
+ * sect		: '[' sectspec ']'
+ * sectspec	: sectmsgtext
+ *		| sectpart
+ *		| sectpart '.' secttext
+ * sectpart	: nz-number
+ *		| sectpart '.' nz-number
+ */
+Nlist*
+mknlist(void)
+{
+	Nlist *nl;
+
+	nl = binalloc(&parsebin, sizeof *nl, 1);
+	if(nl == nil)
+		parseerr("out of memory");
+	nl->n = number(1);
+	return nl;
+}
+
+static Fetch*
+fetchatt(char *s, Fetch *f)
+{
+	int c;
+	Nlist *n;
+
+	if(cistrcmp(s, "envelope") == 0)
+		return mkfetch(Fenvelope, f);
+	if(cistrcmp(s, "flags") == 0)
+		return mkfetch(Fflags, f);
+	if(cistrcmp(s, "internaldate") == 0)
+		return mkfetch(Finternaldate, f);
+	if(cistrcmp(s, "RFC822") == 0)
+		return mkfetch(Frfc822, f);
+	if(cistrcmp(s, "RFC822.header") == 0)
+		return mkfetch(Frfc822head, f);
+	if(cistrcmp(s, "RFC822.size") == 0)
+		return mkfetch(Frfc822size, f);
+	if(cistrcmp(s, "RFC822.text") == 0)
+		return mkfetch(Frfc822text, f);
+	if(cistrcmp(s, "bodystructure") == 0)
+		return mkfetch(Fbodystruct, f);
+	if(cistrcmp(s, "uid") == 0)
+		return mkfetch(Fuid, f);
+
+	if(cistrcmp(s, "body") == 0){
+		if(peekc() != '[')
+			return mkfetch(Fbody, f);
+		f = mkfetch(Fbodysect, f);
+	}else if(cistrcmp(s, "body.peek") == 0)
+		f = mkfetch(Fbodypeek, f);
+	else
+		parseerr("illegal fetch attribute");
+
+	mustbe('[');
+	c = peekc();
+	if(c >= '1' && c <= '9'){
+		n = f->sect = mknlist();
+		while(peekc() == '.'){
+			getc();
+			c = peekc();
+			if(c < '1' || c > '9')
+				break;
+			n->next = mknlist();
+			n = n->next;
+		}
+	}
+	if(peekc() != ']')
+		secttext(f, f->sect != nil);
+	mustbe(']');
+
+	if(peekc() != '<')
+		return f;
+
+	f->partial = 1;
+	mustbe('<');
+	f->start = number(0);
+	mustbe('.');
+	f->size = number(1);
+	mustbe('>');
+	return f;
+}
+
+/*
+ * secttext	: sectmsgtext | "MIME"
+ * sectmsgtext	: "HEADER"
+ *		| "TEXT"
+ *		| "HEADER.FIELDS" ' ' hdrlist
+ *		| "HEADER.FIELDS.NOT" ' ' hdrlist
+ * hdrlist	: '(' hdrs ')'
+ * hdrs:	: astring
+ *		| hdrs ' ' astring
+ */
+static void
+secttext(Fetch *f, int mimeok)
+{
+	char *s;
+	Slist *h;
+
+	s = atomstring(fetchatom, "");
+	if(cistrcmp(s, "header") == 0){
+		f->part = FPhead;
+		return;
+	}
+	if(cistrcmp(s, "text") == 0){
+		f->part = FPtext;
+		return;
+	}
+	if(mimeok && cistrcmp(s, "mime") == 0){
+		f->part = FPmime;
+		return;
+	}
+	if(cistrcmp(s, "header.fields") == 0)
+		f->part = FPheadfields;
+	else if(cistrcmp(s, "header.fields.not") == 0)
+		f->part = FPheadfieldsnot;
+	else
+		parseerr("illegal fetch section text");
+	mustbe(' ');
+	mustbe('(');
+	h = nil;
+	for(;;){
+		h = mkslist(astring(), h);
+		if(peekc() == ')')
+			break;
+		mustbe(' ');
+	}
+	mustbe(')');
+	f->hdrs = revslist(h);
+}
+
+/*
+ * searchwhat	: "CHARSET" ' ' astring searchkeys | searchkeys
+ * searchkeys	: searchkey | searchkeys ' ' searchkey
+ * searchkey	: "ALL" | "ANSWERED" | "DELETED" | "FLAGGED" | "NEW" | "OLD" | "RECENT"
+ *		| "SEEN" | "UNANSWERED" | "UNDELETED" | "UNFLAGGED" | "DRAFT" | "UNDRAFT"
+ *		| astrkey ' ' astring
+ *		| datekey ' ' date
+ *		| "KEYWORD" ' ' flag | "UNKEYWORD" flag
+ *		| "LARGER" ' ' number | "SMALLER" ' ' number
+ * 		| "HEADER" astring ' ' astring
+ *		| set | "UID" ' ' set
+ *		| "NOT" ' ' searchkey
+ *		| "OR" ' ' searchkey ' ' searchkey
+ *		| '(' searchkeys ')'
+ * astrkey	: "BCC" | "BODY" | "CC" | "FROM" | "SUBJECT" | "TEXT" | "TO"
+ * datekey	: "BEFORE" | "ON" | "SINCE" | "SENTBEFORE" | "SENTON" | "SENTSINCE"
+ */
+static Namedint searchmap[] =
+{
+	{"ALL",		SKall},
+	{"ANSWERED",	SKanswered},
+	{"DELETED",	SKdeleted},
+	{"FLAGGED",	SKflagged},
+	{"NEW",		SKnew},
+	{"OLD",		SKold},
+	{"RECENT",	SKrecent},
+	{"SEEN",	SKseen},
+	{"UNANSWERED",	SKunanswered},
+	{"UNDELETED",	SKundeleted},
+	{"UNFLAGGED",	SKunflagged},
+	{"DRAFT",	SKdraft},
+	{"UNDRAFT",	SKundraft},
+	{"UNSEEN",	SKunseen},
+	{nil,		0}
+};
+
+static Namedint searchmapstr[] =
+{
+	{"CHARSET",	SKcharset},
+	{"BCC",		SKbcc},
+	{"BODY",	SKbody},
+	{"CC",		SKcc},
+	{"FROM",	SKfrom},
+	{"SUBJECT",	SKsubject},
+	{"TEXT",	SKtext},
+	{"TO",		SKto},
+	{nil,		0}
+};
+
+static Namedint searchmapdate[] =
+{
+	{"BEFORE",	SKbefore},
+	{"ON",		SKon},
+	{"SINCE",	SKsince},
+	{"SENTBEFORE",	SKsentbefore},
+	{"SENTON",	SKsenton},
+	{"SENTSINCE",	SKsentsince},
+	{nil,		0}
+};
+
+static Namedint searchmapflag[] =
+{
+	{"KEYWORD",	SKkeyword},
+	{"UNKEYWORD",	SKunkeyword},
+	{nil,		0}
+};
+
+static Namedint searchmapnum[] =
+{
+	{"SMALLER",	SKsmaller},
+	{"LARGER",	SKlarger},
+	{nil,		0}
+};
+
+static Search*
+searchkeys(int first, Search *tail)
+{
+	Search *s;
+
+	for(;;){
+		if(peekc() == '('){
+			getc();
+			tail = searchkeys(0, tail);
+			mustbe(')');
+		}else{
+			s = searchkey(first);
+			tail->next = s;
+			tail = s;
+		}
+		first = 0;
+		if(peekc() != ' ')
+			break;
+		getc();
+	}
+	return tail;
+}
+
+static Search*
+searchkey(int first)
+{
+	char *a;
+	int i, c;
+	Search *sr, rock;
+	Tm tm;
+
+	sr = binalloc(&parsebin, sizeof *sr, 1);
+	if(sr == nil)
+		parseerr("out of memory");
+
+	c = peekc();
+	if(c >= '0' && c <= '9'){
+		sr->key = SKset;
+		sr->set = msgset(0);
+		return sr;
+	}
+
+	a = atom();
+	if(i = mapint(searchmap, a))
+		sr->key = i;
+	else if(i = mapint(searchmapstr, a)){
+		if(!first && i == SKcharset)
+			parseerr("illegal search key");
+		sr->key = i;
+		mustbe(' ');
+		sr->s = astring();
+	}else if(i = mapint(searchmapdate, a)){
+		sr->key = i;
+		mustbe(' ');
+		c = peekc();
+		if(c == '"')
+			getc();
+		a = atom();
+		if(a == nil || !imap4date(&tm, a))
+			parseerr("bad date format");
+		sr->year = tm.year;
+		sr->mon = tm.mon;
+		sr->mday = tm.mday;
+		if(c == '"')
+			mustbe('"');
+	}else if(i = mapint(searchmapflag, a)){
+		sr->key = i;
+		mustbe(' ');
+		c = peekc();
+		if(c == '\\'){
+			mustbe('\\');
+			a = atomstring(atomstop, "\\");
+		}else
+			a = atom();
+		i = mapflag(a);
+		if(i == 0)
+			parseerr("flag not supported");
+		sr->num = i;
+	}else if(i = mapint(searchmapnum, a)){
+		sr->key = i;
+		mustbe(' ');
+		sr->num = number(0);
+	}else if(cistrcmp(a, "HEADER") == 0){
+		sr->key = SKheader;
+		mustbe(' ');
+		sr->hdr = astring();
+		mustbe(' ');
+		sr->s = astring();
+	}else if(cistrcmp(a, "UID") == 0){
+		sr->key = SKuid;
+		mustbe(' ');
+		sr->set = msgset(0);
+	}else if(cistrcmp(a, "NOT") == 0){
+		sr->key = SKnot;
+		mustbe(' ');
+		rock.next = nil;
+		searchkeys(0, &rock);
+		sr->left = rock.next;
+	}else if(cistrcmp(a, "OR") == 0){
+		sr->key = SKor;
+		mustbe(' ');
+		rock.next = nil;
+		searchkeys(0, &rock);
+		sr->left = rock.next;
+		mustbe(' ');
+		rock.next = nil;
+		searchkeys(0, &rock);
+		sr->right = rock.next;
+	}else
+		parseerr("illegal search key");
+	return sr;
+}
+
+/*
+ * set	: seqno
+ *	| seqno ':' seqno
+ *	| set ',' set
+ * seqno: nz-number
+ *	| '*'
+ *
+ */
+static Msgset*
+msgset(int uids)
+{
+	uint from, to;
+	Msgset head, *last, *ms;
+
+	last = &head;
+	head.next = nil;
+	for(;;){
+		from = uids ? uidno() : seqno();
+		to = from;
+		if(peekc() == ':'){
+			getc();
+			to = uids? uidno(): seqno();
+		}
+		ms = binalloc(&parsebin, sizeof *ms, 0);
+		if(ms == nil)
+			parseerr("out of memory");
+		if(to < from){
+			ms->from = to;
+			ms->to = from;
+		}else{
+			ms->from = from;
+			ms->to = to;
+		}
+		ms->next = nil;
+		last->next = ms;
+		last = ms;
+		if(peekc() != ',')
+			break;
+		getc();
+	}
+	return head.next;
+}
+
+static uint
+seqno(void)
+{
+	if(peekc() == '*'){
+		getc();
+		return ~0UL;
+	}
+	return number(1);
+}
+
+static uint
+uidno(void)
+{
+	if(peekc() == '*'){
+		getc();
+		return ~0UL;
+	}
+	return number(0);
+}
+
+/*
+ * 7 bit, non-ctl chars, no (){%*"\
+ * NIL is special case for nstring or parenlist
+ */
+static char *
+atom(void)
+{
+	return atomstring(atomstop, "");
+}
+
+/*
+ * like an atom, but no +
+ */
+static char *
+tag(void)
+{
+	return atomstring("+(){%*\"\\", "");
+}
+
+/*
+ * string or atom allowing %*
+ */
+static char *
+listmbox(void)
+{
+	int c;
+
+	c = peekc();
+	if(c == '{')
+		return literal();
+	if(c == '"')
+		return quoted();
+	return atomstring("(){\"\\", "");
+}
+
+/*
+ * string or atom
+ */
+static char *
+astring(void)
+{
+	int c;
+
+	c = peekc();
+	if(c == '{')
+		return literal();
+	if(c == '"')
+		return quoted();
+	return atom();
+}
+
+/*
+ * 7 bit, non-ctl chars, none from exception list
+ */
+static char *
+atomstring(char *disallowed, char *initial)
+{
+	char *s;
+	int c, ns, as;
+
+	ns = strlen(initial);
+	s = binalloc(&parsebin, ns + Stralloc, 0);
+	if(s == nil)
+		parseerr("out of memory");
+	strcpy(s, initial);
+	as = ns + Stralloc;
+	for(;;){
+		c = getc();
+		if(c <= ' ' || c >= 0x7f || strchr(disallowed, c) != nil){
+			ungetc();
+			break;
+		}
+		s[ns++] = c;
+		if(ns >= as){
+			s = bingrow(&parsebin, s, as, as + Stralloc, 0);
+			if(s == nil)
+				parseerr("out of memory");
+			as += Stralloc;
+		}
+	}
+	if(ns == 0)
+		badsyn();
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * quoted: '"' chars* '"'
+ * chars:	1-128 except \r and \n
+ */
+static char *
+quoted(void)
+{
+	char *s;
+	int c, ns, as;
+
+	mustbe('"');
+	s = binalloc(&parsebin, Stralloc, 0);
+	if(s == nil)
+		parseerr("out of memory");
+	as = Stralloc;
+	ns = 0;
+	for(;;){
+		c = getc();
+		if(c == '"')
+			break;
+		if(c < 1 || c > 0x7f || c == '\r' || c == '\n')
+			badsyn();
+		if(c == '\\'){
+			c = getc();
+			if(c != '\\' && c != '"')
+				badsyn();
+		}
+		s[ns++] = c;
+		if(ns >= as){
+			s = bingrow(&parsebin, s, as, as + Stralloc, 0);
+			if(s == nil)
+				parseerr("out of memory");
+			as += Stralloc;
+		}
+	}
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * litlen: {number}\r\n
+ */
+static uint
+litlen(void)
+{
+	uint v;
+
+	mustbe('{');
+	v = number(0);
+	mustbe('}');
+	crnl();
+	return v;
+}
+
+/*
+ * literal: litlen data<0:litlen>
+ */
+static char*
+literal(void)
+{
+	char *s;
+	uint v;
+
+	v = litlen();
+	s = binalloc(&parsebin, v + 1, 0);
+	if(s == nil)
+		parseerr("out of memory");
+	Bprint(&bout, "+ Ready for literal data\r\n");
+	if(Bflush(&bout) < 0)
+		writeerr();
+	if(v != 0 && Bread(&bin, s, v) != v)
+		badsyn();
+	s[v] = '\0';
+	return s;
+}
+
+/*
+ * digits; number is 32 bits
+ */
+enum{
+	Max = 0xffffffff/10,
+};
+
+static uint
+number(int nonzero)
+{
+	uint n, nn;
+	int c, first, ovfl;
+
+	n = 0;
+	first = 1;
+	ovfl = 0;
+	for(;;){
+		c = getc();
+		if(c < '0' || c > '9'){
+			ungetc();
+			if(first)
+				badsyn();
+			break;
+		}
+		c -= '0';
+		first = 0;
+		if(n > Max)
+			ovfl = 1;
+		nn = n*10 + c;
+		if(nn < n)
+			ovfl = 1;
+		n = nn;
+	}
+	if(nonzero && n == 0)
+		badsyn();
+	if(ovfl)
+		parseerr("number out of range\r\n");
+	return n;
+}
+
+static void
+logit(char *o)
+{
+	char *s, *p;
+
+	if(!debug)
+		return;
+	s = strdup(o);
+	p = strchr(s, ' ');
+	if(!p)
+		goto emit;
+	if(cistrncmp(p, "login ", 6) == 0){
+		free(s);
+		return;
+	}
+emit:
+	for(p = s + strlen(s) - 1; p >= s && (/**p == '\r' ||*/ *p == '\n'); )
+		*p-- = 0;
+	ilog("%s", s);
+	free(s);
+}
+
+static char *gbuf;
+static char *gbufp = "";
+
+static int
+getc(void)
+{
+	if(*gbufp == 0){
+		free(gbuf);
+		werrstr("");
+		gbufp = gbuf = Brdstr(&bin, '\n', 0);
+		if(gbuf == 0){
+			ilog("empty line [%d]: %r", bin.fid);
+			gbufp = "";
+			return -1;
+		}
+		logit(gbuf);
+	}
+	return *gbufp++;
+}
+
+static void
+ungetc(void)
+{
+	if(gbufp > gbuf)
+		gbufp--;
+}
+
+static int
+peekc(void)
+{
+	return *gbufp;
+}
+
+#ifdef normal
+
+static int
+getc(void)
+{
+	return Bgetc(&bin);
+}
+
+static void
+ungetc(void)
+{
+	Bungetc(&bin);
+}
+
+static int
+peekc(void)
+{
+	int c;
+
+	c = Bgetc(&bin);
+	Bungetc(&bin);
+	return c;
+}
+#endif
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/imap4d.h
@@ -1,0 +1,392 @@
+/*
+ * mailbox and message representations
+ *
+ * these structures are allocated with emalloc and must be explicitly freed
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <auth.h>
+#include <avl.h>
+#include <bin.h>
+
+typedef struct Box	Box;
+typedef struct Header	Header;
+typedef struct Maddr	Maddr;
+typedef struct Mblock	Mblock;
+typedef struct Mimehdr	Mimehdr;
+typedef struct Msg	Msg;
+typedef struct Namedint	Namedint;
+typedef struct Pair	Pair;
+typedef struct Uidplus	Uidplus;
+
+enum
+{
+	Stralloc		= 32,		/* characters allocated at a time */
+	Bufsize		= 8*1024,	/* size of transfer block */
+	Ndigest		= 40,		/* length of digest string */
+	Nuid		= 10,		/* length of .imp uid string */
+	Nflags		= 8,		/* length of .imp flag string */
+	Locksecs		= 5 * 60,	/* seconds to wait for acquiring a locked file */
+	Pathlen		= 256,		/* max. length of upas/fs mbox name */
+	Filelen		= 32,		/* max. length of a file in a upas/fs mbox */
+	Userlen		= 64,		/* max. length of user's name */
+
+	Mutf7max	= 6,		/* max bytes for a mutf7 character: &bbbb- */
+
+	/*
+	 * message flags
+	 */
+	Fseen		= 1 << 0,
+	Fanswered	= 1 << 1,
+	Fflagged	= 1 << 2,
+	Fdeleted	= 1 << 3,
+	Fdraft		= 1 << 4,
+	Frecent		= 1 << 5,
+};
+
+typedef struct Fstree Fstree;
+struct Fstree {
+	Avl;
+	Msg *m;
+};
+
+struct Box
+{
+	char	*name;		/* path name of mailbox */
+	char	*fs;		/* fs name of mailbox */
+	char	*fsdir;		/* /mail/fs/box->fs */
+	char	*imp;		/* path name of .imp file */
+	uchar	writable;	/* can write back messages? */
+	uchar	dirtyimp;	/* .imp file needs to be written? */
+	uchar	sendflags;	/* need flags update */
+	Qid	qid;		/* qid of fs mailbox */
+	Qid	impqid;		/* qid of .imp when last synched */
+	long	mtime;		/* file mtime when last read */
+	uint	max;		/* maximum msgs->seq, same as number of messages */
+	uint	toldmax;	/* last value sent to client */
+	uint	recent;		/* number of recently received messaged */
+	uint	toldrecent;	/* last value sent to client */
+	uint	uidnext;	/* next uid value assigned to a message */
+	uint	uidvalidity;	/* uid of mailbox */
+	Msg	*msgs;		/* msgs in uid order */
+	Avltree	*fstree;		/* msgs in upas/fs order */
+};
+
+/*
+ * fields of Msg->info
+ */
+enum
+{
+	/*
+	 * read from upasfs
+	 */
+	Ifrom,
+	Ito,
+	Icc,
+	Ireplyto,
+	Iunixdate,
+	Isubject,
+	Itype,
+	Idisposition,
+	Ifilename,
+	Idigest,
+	Ibcc,
+	Iinreplyto,
+	Idate,
+	Isender,
+	Imessageid,
+	Ilines,		/* number of lines of raw body */
+	Isize,
+//	Iflags,
+//	Idatesec
+
+	Imax
+};
+
+struct Header
+{
+	char	*buf;		/* header, including terminating \r\n */
+	uint	size;		/* strlen(buf) */
+	uint	lines;		/* number of \n characters in buf */
+
+	/*
+	 * pre-parsed mime headers
+	 */
+	Mimehdr	*type;		/* content-type */
+	Mimehdr	*id;		/* content-id */
+	Mimehdr	*description;	/* content-description */
+	Mimehdr	*encoding;	/* content-transfer-encoding */
+//	Mimehdr	*md5;		/* content-md5 */
+	Mimehdr	*disposition;	/* content-disposition */
+	Mimehdr	*language;	/* content-language */
+};
+
+struct Msg
+{
+	Msg	*next;
+	Msg	*kids;
+	Msg	*parent;
+	char	*fsdir;		/* box->fsdir of enclosing message */
+	Header	head;		/* message header */
+	Header	mime;		/* mime header from enclosing multipart spec */
+	int	flags;
+	uchar	sendflags;	/* flags value needs to be sent to client */
+	uchar	expunged;	/* message actually expunged, but not yet reported to client */
+	uchar	matched;	/* search succeeded? */
+	uint	uid;		/* imap unique identifier */
+	uint	seq;		/* position in box; 1 is oldest */
+	uint	id;		/* number of message directory in upas/fs */
+	char	*fs;		/* name of message directory */
+	char	*efs;		/* pointer after / in fs; enough space for file name */
+
+	uint	size;		/* size of fs/rawbody, in bytes, with \r added before \n */
+	uint	lines;		/* number of lines in rawbody */
+
+	char	*ibuf;
+	char	*info[Imax];	/* all info about message */
+
+	Maddr	*to;		/* parsed out address lines */
+	Maddr	*from;
+	Maddr	*replyto;
+	Maddr	*sender;
+	Maddr	*cc;
+	Maddr	*bcc;
+};
+
+/*
+ * pre-parsed header lines
+ */
+struct Maddr
+{
+	char	*personal;
+	char	*box;
+	char	*host;
+	Maddr	*next;
+};
+
+struct Mimehdr
+{
+	char	*s;
+	char	*t;
+	Mimehdr	*next;
+};
+
+/*
+ * mapping of integer & names
+ */
+struct Namedint
+{
+	char	*name;
+	int	v;
+};
+
+/*
+ * lock for all mail file operations
+ */
+struct Mblock
+{
+	int	fd;
+};
+
+/*
+ * parse nodes for imap4rev1 protocol
+ *
+ * important: all of these items are allocated
+ * in one can, so they can be tossed out at the same time.
+ * this allows leakless parse error recovery by simply tossing the can away.
+ * however, it means these structures cannot be mixed with the mailbox structures
+ */
+
+typedef struct Fetch	Fetch;
+typedef struct Nlist	Nlist;
+typedef struct Slist	Slist;
+typedef struct Msgset	Msgset;
+typedef struct Store	Store;
+typedef struct Search	Search;
+
+/*
+ * parse tree for fetch command
+ */
+enum
+{
+	Fenvelope,
+	Fflags,
+	Finternaldate,
+	Frfc822,
+	Frfc822head,
+	Frfc822size,
+	Frfc822text,
+	Fbodystruct,
+	Fuid,
+	Fbody,			/* BODY */
+	Fbodysect,		/* BODY [...] */
+	Fbodypeek,
+
+	Fmax
+};
+
+enum
+{
+	FPall,
+	FPhead,
+	FPheadfields,
+	FPheadfieldsnot,
+	FPmime,
+	FPtext,
+
+	FPmax
+};
+
+struct Fetch
+{
+	uchar	op;		/* F.* operator */
+	uchar	part;		/* FP.* subpart for body[] & body.peek[]*/
+	uchar	partial;	/* partial fetch? */
+	long	start;		/* partial fetch amounts */
+	long	size;
+	Nlist	*sect;
+	Slist	*hdrs;
+	Fetch	*next;
+};
+
+/*
+ * status items
+ */
+enum{
+	Smessages	= 1 << 0,
+	Srecent		= 1 << 1,
+	Suidnext		= 1 << 2,
+	Suidvalidity	= 1 << 3,
+	Sunseen		= 1 << 4,
+};
+
+/*
+ * parse tree for store command
+ */
+enum
+{
+	Stflags,
+	Stflagssilent,
+
+	Stmax
+};
+
+struct Store
+{
+	uchar	sign;
+	uchar	op;
+	int	flags;
+};
+
+/*
+ * parse tree for search command
+ */
+enum
+{
+	SKnone,
+
+	SKcharset,
+
+	SKall,
+	SKanswered,
+	SKbcc,
+	SKbefore,
+	SKbody,
+	SKcc,
+	SKdeleted,
+	SKdraft,
+	SKflagged,
+	SKfrom,
+	SKheader,
+	SKkeyword,
+	SKlarger,
+	SKnew,
+	SKnot,
+	SKold,
+	SKon,
+	SKor,
+	SKrecent,
+	SKseen,
+	SKsentbefore,
+	SKsenton,
+	SKsentsince,
+	SKset,
+	SKsince,
+	SKsmaller,
+	SKsubject,
+	SKtext,
+	SKto,
+	SKuid,
+	SKunanswered,
+	SKundeleted,
+	SKundraft,
+	SKunflagged,
+	SKunkeyword,
+	SKunseen,
+
+	SKmax
+};
+
+struct Search
+{
+	int	key;
+	char	*s;
+	char	*hdr;
+	uint	num;
+	int	year;
+	int	mon;
+	int	mday;
+	Msgset	*set;
+	Search	*left;
+	Search	*right;
+	Search	*next;
+};
+
+struct Nlist
+{
+	uint	n;
+	Nlist	*next;
+};
+
+struct Slist
+{
+	char	*s;
+	Slist	*next;
+};
+
+struct Msgset
+{
+	uint	from;
+	uint	to;
+	Msgset	*next;
+};
+
+struct Pair
+{
+	uint	start;
+	uint	stop;
+};
+
+struct Uidplus
+{
+	uint	uid;
+	uint	uidvalidity;
+	Uidplus	*next;
+};
+
+extern	Bin	*parsebin;
+extern	Biobuf	bout;
+extern	Biobuf	bin;
+extern	char	username[Userlen];
+extern	char	mboxdir[Pathlen];
+extern	char	*fetchpartnames[FPmax];
+extern	char	*binupas;
+extern	char	*site;
+extern	char	*remote;
+extern	int	debug;
+extern	char	logfile[28];
+extern	Uidplus	*uidlist;
+extern	Uidplus	**uidtl;
+
+#include "fns.h"
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/imp.c
@@ -1,0 +1,315 @@
+#include "imap4d.h"
+
+static	char	magic[]	= "imap internal mailbox description\n";
+
+/* another appearance of this nasty hack. */
+typedef struct{
+	Avl;
+	Msg	*m;
+}Mtree;
+
+static	Avltree	*mtree;
+static	Bin	*mbin;
+
+static int
+mtreecmp(Avl *va, Avl *vb)
+{
+	Mtree *a, *b;
+
+	a = (Mtree*)va;
+	b = (Mtree*)vb;
+	return strcmp(a->m->info[Idigest], b->m->info[Idigest]);
+}
+
+static Namedint	flagcmap[Nflags] =
+{
+	{"s",	Fseen},
+	{"a",	Fanswered},
+	{"f",	Fflagged},
+	{"D",	Fdeleted},
+	{"d",	Fdraft},
+	{"r",	Frecent},
+};
+
+static int
+parseflags(char *flags)
+{
+	int i, f;
+
+	f = 0;
+	for(i = 0; i < Nflags; i++){
+		if(flags[i] == '-')
+			continue;
+		if(flags[i] != flagcmap[i].name[0])
+			return 0;
+		f |= flagcmap[i].v;
+	}
+	return f;
+}
+
+static int
+impflags(Box *box, Msg *m, char *flags)
+{
+	int f;
+
+	f = parseflags(flags);
+	/*
+	 * recent flags are set until the first time message's box is selected or examined.
+	 * it may be stored in the file as a side effect of a status or subscribe command;
+	 * if so, clear it out.
+	 */
+	if((f & Frecent) && strcmp(box->fs, "imap") == 0)
+		box->dirtyimp = 1;
+	f |= m->flags & Frecent;
+
+	/*
+	 * all old messages with changed flags should be reported to the client
+	 */
+	if(m->uid && m->flags != f){
+		box->sendflags = 1;
+		m->sendflags = 1;
+	}
+	m->flags = f;
+	return 1;
+}
+
+/*
+ * considerations:
+ * . messages can be deleted by another agent
+ * . we might still have a Msg for an expunged message,
+ *	because we haven't told the client yet.
+ * . we can have a Msg without a .imp entry.
+ * . flag information is added at the end of the .imp by copy & append
+ */
+
+static int
+rdimp(Biobuf *b, Box *box)
+{
+	char *s, *f[4];
+	uint u;
+	Msg *m, m0;
+	Mtree t, *p;
+
+	memset(&m0, 0, sizeof m0);
+	for(; s = Brdline(b, '\n'); ){
+		s[Blinelen(b) - 1] = 0;
+		if(tokenize(s, f, nelem(f)) != 3)
+			return -1;
+		u = strtoul(f[1], 0, 10);
+
+		memset(&t, 0, sizeof t);
+		m0.info[Idigest] = f[0];
+		t.m = &m0;
+		p = (Mtree*)avllookup(mtree, &t, 0);
+		if(p){
+			m = p->m;
+			if(m->uid && m->uid != u){
+				ilog("dup? %ud %ud %s", u, m->uid, f[0]);
+				continue;
+			}
+			if(m->uid >= box->uidnext){
+				ilog("uid %ud >= %ud\n", m->uid, box->uidnext);
+				box->uidnext = m->uid;
+			}
+			if(m->uid == 0)
+				m->flags = 0;
+			if(impflags(box, m, f[2]) == -1)
+				return -1;
+			m->uid = u;
+		}else{
+			/*
+			 * message has been deleted.
+			 */
+//			ilog("flags, uid dropped on floor [%s, %ud]", m0.info[Idigest], u);
+		}
+	}
+	return 0;
+}
+
+enum{
+	Rmagic,
+	Rrdstr,
+	Rtok,
+	Rvalidity,
+	Ruidnext,
+};
+
+static char *rtab[] = {
+	"magic",
+	"rdstr",
+	"tok",
+	"val",
+	"uidnext"
+};
+
+char*
+sreason(int r)
+{
+	if(r >= 0 && r <= nelem(rtab))
+		return rtab[r];
+	return "*GOK*";
+}
+
+static int
+verscmp(Biobuf *b, Box *box, int *reason)
+{
+	char *s, *f[3];
+	int n;
+	uint u, v;
+
+	n = -1;
+	*reason = Rmagic;
+	if(s = Brdstr(b, '\n', 0))
+		n = strcmp(s, magic);
+	free(s);
+	if(n == -1)
+		return -1;
+	n = -1;
+	v = box->uidvalidity;
+	if((s = Brdstr(b, '\n', 1)) && ++*reason)
+	if(tokenize(s, f, nelem(f)) == 2 && ++*reason)
+	if((u = strtoul(f[0], 0, 10)) == v || v == 0 && ++*reason)
+	if((v = strtoul(f[1], 0, 10)) >= box->uidnext && ++*reason){
+		box->uidvalidity = u;
+		box->uidnext = v;
+		n = 0;
+	}
+	free(s);
+	return n;
+}
+
+int
+parseimp(Biobuf *b, Box *box)
+{
+	int r, reason;
+	Msg *m;
+	Mtree *p;
+
+	if(verscmp(b, box, &reason) == -1)
+		return -1;
+	mtree = avlcreate(mtreecmp);
+	r = 0;
+	for(m = box->msgs; m; m = m->next)
+		r++;
+	p = binalloc(&mbin, r*sizeof *p, 1);
+	if(p == nil)
+		bye("no memory");
+	for(m = box->msgs; m; m = m->next){
+		p->m = m;
+		avlinsert(mtree, p);
+		p++;
+	}
+	r = rdimp(b, box);
+	binfree(&mbin);
+	free(mtree);
+	return r;
+}
+
+static void
+wrimpflags(char *buf, int flags, int killrecent)
+{
+	int i;
+
+	if(killrecent)
+		flags &= ~Frecent;
+	memset(buf, '-', Nflags);
+	for(i = 0; i < Nflags; i++)
+		if(flags & flagcmap[i].v)
+			buf[i] = flagcmap[i].name[0];
+	buf[i] = 0;
+}
+
+int
+wrimp(Biobuf *b, Box *box)
+{
+	char buf[16];
+	int i;
+	Msg *m;
+
+	box->dirtyimp = 0;
+	Bprint(b, "%s", magic);
+	Bprint(b, "%.*ud %.*ud\n", Nuid, box->uidvalidity, Nuid, box->uidnext);
+	i = strcmp(box->fs, "imap") == 0;
+	for(m = box->msgs; m != nil; m = m->next){
+		if(m->expunged)
+			continue;
+		wrimpflags(buf, m->flags, i);
+		Bprint(b, "%.*s %.*ud %s\n", Ndigest, m->info[Idigest], Nuid, m->uid, buf);
+	}
+	return 0;
+}
+
+static uint
+scanferdup(Biobuf *b, char *digest, int *flags, vlong *pos)
+{
+	char *s, *f[4];
+	uint uid;
+
+	uid = 0;
+	for(; s = Brdline(b, '\n'); ){
+		s[Blinelen(b) - 1] = 0;
+		if(tokenize(s, f, nelem(f)) != 3)
+			return ~0;
+		if(strcmp(f[0], digest) == 0){
+			uid = strtoul(f[1], 0, 10);
+//			fprint(2, "digest %s matches uid %ud\n", f[0], uid);
+			*flags |= parseflags(f[2]);
+			break;
+		}
+		*pos += Blinelen(b);
+	}
+	return uid;
+}
+
+int
+appendimp(char *bname, char *digest, int flags, Uidplus *u)
+{
+	char buf[16], *iname;
+	int fd, reason;
+	uint dup;
+	vlong pos;
+	Biobuf b;
+	Box box;
+
+	dup = 0;
+	pos = 0;
+	memset(&box, 0, sizeof box);
+	iname = impname(bname);
+	fd = cdopen(mboxdir, iname, ORDWR);
+	if(fd == -1){
+		fd = cdcreate(mboxdir, iname, OWRITE, 0664);
+		if(fd == -1)
+			return -1;
+		box.uidvalidity = time(0);
+		box.uidnext = 1;
+	}else{
+		dup = ~0;
+		Binit(&b, fd, OREAD);
+		if(verscmp(&b, &box, &reason) == -1)
+			ilog("bad verscmp %s", sreason(reason));
+		else{
+			pos = Bseek(&b, 0, 1);
+			dup = scanferdup(&b, digest, &flags, &pos);
+		}
+		Bterm(&b);
+	}
+	if(dup == ~0){
+		close(fd);
+		return -1;
+	}
+	Binit(&b, fd, OWRITE);
+	if(dup == 0){
+		Bseek(&b, 0, 0);
+		Bprint(&b, "%s", magic);
+		Bprint(&b, "%.*ud %.*ud\n", Nuid, box.uidvalidity, Nuid, box.uidnext + 1);
+		Bseek(&b, 0, 2);
+	}else
+ 		Bseek(&b, pos, 0);
+	wrimpflags(buf, flags, 0);
+	Bprint(&b, "%.*s %.*ud %s\n", Ndigest, digest, Nuid, dup? dup: box.uidnext, buf);
+	Bterm(&b);
+	close(fd);
+	u->uidvalidity = box.uidvalidity;
+	u->uid = box.uidnext;
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/list.c
@@ -1,0 +1,425 @@
+#include "imap4d.h"
+
+enum{
+	Mfolder	= 0,
+	Mbox,
+	Mdir,
+};
+
+	char	subscribed[] = "imap.subscribed";
+static	int	ldebug;
+
+#define	dprint(...)	if(ldebug)fprint(2, __VA_ARGS__); else {}
+
+static int	lmatch(char*, char*, char*);
+
+static int
+mopen(char *box, int mode)
+{
+	char buf[Pathlen];
+
+	if(!strcmp(box, "..") || strstr(box, "/.."))
+		return -1;
+	return cdopen(mboxdir, encfs(buf, sizeof buf, box), mode);
+}
+
+static Dir*
+mdirstat(char *box)
+{
+	char buf[Pathlen];
+
+	return cddirstat(mboxdir, encfs(buf, sizeof buf, box));
+}
+
+static long
+mtime(char *box)
+{
+	long mtime;
+	Dir *d;
+
+	mtime = 0;
+	if(d = mdirstat(box))
+		mtime = d->mtime;
+	free(d);
+	return mtime;
+}
+
+static int
+mokmbox(char *s)
+{
+	char *p;
+
+	if(p = strrchr(s, '/'))
+		s = p + 1;
+	if(!strcmp(s, "mbox"))
+		return 1;
+	return okmbox(s);
+}
+
+/*
+ * paranoid check to prevent accidents
+ */
+/*
+ * BOTCH: we're taking it upon ourselves to
+ * identify mailboxes.  this is a bad idea.
+ * keep in sync with ../fs/mdir.c
+ */
+static int
+dirskip(Dir *a, uvlong *uv)
+{
+	char *p;
+
+	if(a->length == 0)
+		return 1;
+	*uv = strtoul(a->name, &p, 0);
+	if(*uv < 1000000 || *p != '.')
+		return 1;
+	*uv = *uv<<8 | strtoul(p+1, &p, 10);
+	if(*p)
+		return 1;
+	return 0;
+}
+
+static int
+chkmbox(char *path, int mode)
+{
+	char buf[32];
+	int i, r, n, fd, type;
+	uvlong uv;
+	Dir *d;
+
+	type = Mbox;
+	if(mode & DMDIR)
+		type = Mdir;
+	fd = mopen(path, OREAD);
+	if(fd == -1)
+		return -1;
+	r = -1;
+	if(type == Mdir && (n = dirread(fd, &d)) > 0){
+		r = Mfolder;
+		for(i = 0; i < n; i++)
+			if(!dirskip(d + i, &uv)){
+				r = Mdir;
+				break;
+			}
+		free(d);
+	}else if(type == Mdir)
+		r = Mdir;
+	else if(type == Mbox){
+		if(pread(fd, buf, sizeof buf, 0) == sizeof buf)
+		if(!strncmp(buf, "From ", 5))
+			r = Mbox;
+	}
+	close(fd);
+	return r;
+}
+
+static int
+chkmboxpath(char *f)
+{
+	int r;
+	Dir *d;
+
+	r = -1;
+	if(d = mdirstat(f))
+		r = chkmbox(f, d->mode);
+	free(d);
+	return r;
+}
+
+static char*
+appendwd(char *nwd, int n, char *wd, char *a)
+{
+	if(wd[0] && a[0] != '/')
+		snprint(nwd, n, "%s/%s", wd, a);
+	else
+		snprint(nwd, n, "%s", a);
+	return nwd;
+}
+
+static int
+output(char *cmd, char *wd, Dir *d, int term)
+{
+	char path[Pathlen], dec[Pathlen], *s, *flags;
+
+	appendwd(path, sizeof path, wd, d->name);
+	dprint("Xoutput %s %s %d\n", wd, d->name, term);
+	switch(chkmbox(path, d->mode)){
+	default:
+		return 0;
+	case Mfolder:
+		flags = "(\\Noselect)";
+		break;
+	case Mdir:
+	case Mbox:
+		s = impname(path);
+		if(s != nil && mtime(s) < d->mtime)
+			flags = "(\\Noinferiors \\Marked)";
+		else
+			flags = "(\\Noinferiors)";
+		break;
+	}
+
+	if(!term)
+		return 1;
+
+	if(s = strmutf7(decfs(dec, sizeof dec, path)))
+		Bprint(&bout, "* %s %s \"/\" %#Z\r\n", cmd, flags, s);
+	return 1;
+}
+
+static int
+rematch(char *cmd, char *wd, char *pat, Dir *d)
+{
+	char nwd[Pathlen];
+
+	appendwd(nwd, sizeof nwd, wd, d->name);
+	if(d->mode & DMDIR)
+	if(chkmbox(nwd, d->mode) == Mfolder)
+	if(lmatch(cmd, pat, nwd))
+		return 1;
+	return 0;
+}
+
+static int
+match(char *cmd, char *wd, char *pat, Dir *d, int i)
+{
+	char *p, *p1;
+	int m, n;
+	Rune r, r1;
+
+	m = 0;
+	for(p = pat; ; p = p1){
+		n = chartorune(&r, p);
+		p1 = p + n;
+		dprint("r = %C [%.2ux]\n", r, r);
+		switch(r){
+		case '*':
+		case '%':
+			for(r1 = 1; r1;){
+				if(match(cmd, wd, p1, d, i))
+				if(output(cmd, wd, d, 0)){
+					m++;
+					break;
+				}
+				i += chartorune(&r1, d->name + i);
+			}
+			if(r == '*' && rematch(cmd, wd, p, d))
+				return 1;
+			if(m > 0)
+				return 1;
+			break;
+		case '/':
+			return rematch(cmd, wd, p1, d);
+		default:
+			chartorune(&r1, d->name + i);
+			if(r1 != r)
+				return 0;
+			if(r == 0)
+				return output(cmd, wd, d, 1);
+			dprint("  r %C ~ %C [%.2ux]\n", r, r1, r1);
+			i += n;
+			break;
+		}
+	}
+}
+
+static int
+lmatch(char *cmd, char *pat, char *wd)
+{
+	char dec[Pathlen];
+	int fd, n, m, i;
+	Dir *d;
+
+	if((fd = mopen(wd[0]? wd: ".", OREAD)) == -1)
+		return -1;
+	if(wd[0])
+		dprint("wd %s\n", wd);
+	m = 0;
+	for(;;){
+		n = dirread(fd, &d);
+		if(n <= 0)
+			break;
+		for(i = 0; i < n; i++)
+			if(mokmbox(d[i].name)){
+				d[i].name = decfs(dec, sizeof dec, d[i].name);
+				m += match(cmd, wd, pat, d + i, 0);
+			}
+		free(d);
+	}
+	close(fd);
+	return m;
+}
+
+int
+listboxes(char *cmd, char *ref, char *pat)
+{
+	char buf[Pathlen];
+
+	pat = appendwd(buf, sizeof buf, ref, pat);
+	return lmatch(cmd, pat, "") > 0;
+}
+
+static int
+opensubscribed(void)
+{
+	int fd;
+
+	fd = cdopen(mboxdir, subscribed, ORDWR);
+	if(fd >= 0)
+		return fd;
+	fd = cdcreate(mboxdir, subscribed, ORDWR, 0664);
+	if(fd < 0)
+		return -1;
+	fprint(fd, "#imap4 subscription list\nINBOX\n");
+	seek(fd, 0, 0);
+	return fd;
+}
+
+/*
+ * resistance to hand-edits
+ */
+static char*
+trim(char *s, int l)
+{
+	int c;
+
+	for(;; l--){
+		if(l == 0)
+			return 0;
+		c = s[l - 1];
+		if(c != '\t' && c != ' ')
+			break;
+	}
+	for(s[l] = 0; c = *s; s++)
+		if(c != '\t' && c != ' ')
+			break;
+	if(c == 0 || c == '#')
+		return 0;
+	return s;
+}
+
+static int
+poutput(char *cmd, char *f, int term)
+{
+	char *p, *wd;
+	int r;
+	Dir *d;
+
+	if(!mokmbox(f) || !(d = mdirstat(f)))
+		return 0;
+	wd = "";
+	if(p = strrchr(f, '/')){
+		*p = 0;
+		wd = f;
+	}
+	r = output(cmd, wd, d, term);
+	if(p)
+		*p = '/';
+	free(d);
+	return r;
+}
+
+static int
+pmatch(char *cmd, char *pat, char *f, int i)
+{
+	char *p, *p1;
+	int m, n;
+	Rune r, r1;
+
+	dprint("pmatch pat[%s] f[%s]\n", pat, f + i);
+	m = 0;
+	for(p = pat; ; p = p1){
+		n = chartorune(&r, p);
+		p1 = p + n;
+		switch(r){
+		case '*':
+		case '%':
+			for(r1 = 1; r1;){
+				if(pmatch(cmd, p1, f, i))
+				if(poutput(cmd, f, 0)){
+					m++;
+					break;
+				}
+				i += chartorune(&r1, f + i);
+				if(r == '%' && r1 == '/')
+					break;
+			}
+			if(m > 0)
+				return 1;
+			break;
+		default:
+			chartorune(&r1, f + i);
+			if(r1 != r)
+				return 0;
+			if(r == 0)
+				return poutput(cmd, f, 1);
+			i += n;
+			break;
+		}
+	}
+}
+
+int
+lsubboxes(char *cmd, char *ref, char *pat)
+{
+	char *s, buf[Pathlen];
+	int r, fd;
+	Biobuf b;
+	Mblock *l;
+
+	pat = appendwd(buf, sizeof buf, ref, pat);
+	if((l = mblock()) == nil)
+		return 0;
+	fd = opensubscribed();
+	r = 0;
+	Binit(&b, fd, OREAD);
+	while(s = Brdline(&b, '\n'))
+		if(s = trim(s, Blinelen(&b) - 1))
+			r += pmatch(cmd, pat, s, 0);
+	Bterm(&b);
+	close(fd);
+	mbunlock(l);
+	return r;
+}
+
+int
+subscribe(char *mbox, int how)
+{
+	char *s, *in, *ein;
+	int fd, tfd, ok, l;
+	Mblock *mb;
+
+	if(cistrcmp(mbox, "inbox") == 0)
+		mbox = "INBOX";
+	if((mb = mblock()) == nil)
+		return 0;
+	fd = opensubscribed();
+	if(fd < 0 || (in = readfile(fd)) == nil){
+		close(fd);
+		mbunlock(mb);
+		return 0;
+	}
+	l = strlen(mbox);
+	s = strstr(in, mbox);
+	while(s != nil && (s != in && s[-1] != '\n' || s[l] != '\n'))
+		s = strstr(s + 1, mbox);
+	ok = 0;
+	if(how == 's' && s == nil){
+		if(chkmboxpath(mbox) > 0)
+		if(fprint(fd, "%s\n", mbox) > 0)
+			ok = 1;
+	}else if(how == 'u' && s != nil){
+		ein = strchr(s, 0);
+		memmove(s, &s[l+1], ein - &s[l+1]);
+		ein -= l + 1;
+		tfd = cdopen(mboxdir, subscribed, OWRITE|OTRUNC);
+		if(tfd >= 0 && pwrite(fd, in, ein - in, 0) == ein - in)
+			ok = 1;
+		close(tfd);
+	}else
+		ok = 1;
+	close(fd);
+	mbunlock(mb);
+	return ok;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/mbox.c
@@ -1,0 +1,630 @@
+#include "imap4d.h"
+
+static	int	fsctl		= -1;
+static	char	Ecanttalk[]	= "can't talk to mail server";
+
+static void
+fsinit(void)
+{
+	if(fsctl != -1)
+		return;
+	fsctl = open("/mail/fs/ctl", ORDWR);
+	if(fsctl == -1)
+		bye(Ecanttalk);
+}
+
+static void
+boxflags(Box *box)
+{
+	Msg *m;
+
+	box->recent = 0;
+	for(m = box->msgs; m != nil; m = m->next){
+		if(m->uid == 0){
+	//		fprint(2, "unassigned uid %s\n", m->info[Idigest]);
+			box->dirtyimp = 1;
+			m->uid = box->uidnext++;
+		}
+		if(m->flags & Frecent)
+			box->recent++;
+	}
+}
+
+/*
+ * try to match permissions with mbox
+ */
+static int
+createimp(Box *box, Qid *qid)
+{
+	int fd;
+	long mode;
+	Dir *d;
+
+	fd = cdcreate(mboxdir, box->imp, OREAD, 0664);
+	if(fd < 0)
+		return -1;
+	d = cddirstat(mboxdir, box->name);
+	if(d != nil){
+		mode = d->mode & 0777;
+		nulldir(d);
+		d->mode = mode;
+		dirfwstat(fd, d);
+		free(d);
+	}
+	if(fqid(fd, qid) < 0){
+		close(fd);
+		return -1;
+	}
+
+	return fd;
+}
+
+/*
+ * read in the .imp file, or make one if it doesn't exist.
+ * make sure all flags and uids are consistent.
+ * return the mailbox lock.
+ */
+static Mblock*
+openimp(Box *box, int new)
+{
+	char buf[ERRMAX];
+	int fd;
+	Biobuf b;
+	Mblock *ml;
+	Qid qid;
+
+	ml = mblock();
+	if(ml == nil)
+		return nil;
+	fd = cdopen(mboxdir, box->imp, OREAD);
+	if(fd < 0 || fqid(fd, &qid) < 0){
+		if(fd < 0){
+			errstr(buf, sizeof buf);
+			if(cistrstr(buf, "does not exist") == nil)
+				ilog("imp: %s: %s", box->imp, buf);
+			else
+				debuglog("imp: %s: %s .. creating", box->imp, buf);
+		}else{
+			close(fd);
+			ilog("%s: bogus imp: bad qid: recreating", box->imp);
+		}
+		fd = createimp(box, &qid);
+		if(fd < 0){
+			ilog("createimp fails: %r");
+			mbunlock(ml);
+			return nil;
+		}
+		box->dirtyimp = 1;
+		if(box->uidvalidity == 0){
+			ilog("set uidvalidity %lud [new]\n", box->uidvalidity);
+			box->uidvalidity = box->mtime;
+		}
+		box->impqid = qid;
+		new = 1;
+	}else if(qid.path != box->impqid.path || qid.vers != box->impqid.vers){
+		Binit(&b, fd, OREAD);
+		if(parseimp(&b, box) == -1){
+			ilog("%s: bogus imp: parse failure", box->imp);
+			box->dirtyimp = 1;
+			if(box->uidvalidity == 0){
+				ilog("set uidvalidity %lud [parseerr]\n", box->uidvalidity);
+				box->uidvalidity = box->mtime;
+			}
+		}
+		Bterm(&b);
+		box->impqid = qid;
+		new = 1;
+	}
+	if(new)
+		boxflags(box);
+	close(fd);
+	return ml;
+}
+
+/*
+ * mailbox is unreachable, so mark all messages expunged
+ * clean up .imp files as well.
+ */
+static void
+mboxgone(Box *box)
+{
+	char buf[ERRMAX];
+	Msg *m;
+
+	rerrstr(buf, ERRMAX);
+	if(strstr(buf, "hungup channel"))
+		bye(Ecanttalk);
+//	too smart.
+//	if(cdexists(mboxdir, box->name) < 0)
+//		cdremove(mboxdir, box->imp);
+	for(m = box->msgs; m != nil; m = m->next)
+		m->expunged = 1;
+	ilog("mboxgone");
+	box->writable = 0;
+}
+
+/*
+ * read messages in the mailbox
+ * mark message that no longer exist as expunged
+ * returns -1 for failure, 0 if no new messages, 1 if new messages.
+ */
+enum {
+	Gone	= 2,		/* don't unexpunge messages */
+};
+
+static int
+readbox(Box *box)
+{
+	char buf[ERRMAX];
+	int i, n, fd, new, id;
+	Dir *d;
+	Msg *m, *last;
+
+	fd = cdopen(box->fsdir, ".", OREAD);
+	if(fd == -1){
+goinggoinggone:
+		rerrstr(buf, ERRMAX);
+		ilog("upas/fs stat of %s/%s aka %s failed: %r",
+			username, box->name, box->fsdir);
+		mboxgone(box);
+		return -1;
+	}
+
+	if((d = dirfstat(fd)) == nil){
+		close(fd);
+		goto goinggoinggone;
+	}
+	box->mtime = d->mtime;
+	box->qid = d->qid;
+	last = nil;
+	for(m = box->msgs; m != nil; m = m->next){
+		last = m;
+		m->expunged |= Gone;
+	}
+	new = 0;
+	free(d);
+
+	for(;;){
+		n = dirread(fd, &d);
+		if(n <= 0){
+			close(fd);
+			if(n == -1)
+				goto goinggoinggone;
+			break;
+		}
+		for(i = 0; i < n; i++){
+			if((d[i].qid.type & QTDIR) == 0)
+				continue;
+			id = atoi(d[i].name);
+			if(m = fstreefind(box, id)){
+				m->expunged &= ~Gone;
+				continue;
+			}
+			new = 1;
+			m = MKZ(Msg);
+			m->id = id;
+			m->fsdir = box->fsdir;
+			m->fs = emalloc(2 * (Filelen + 1));
+			m->efs = seprint(m->fs, m->fs + (Filelen + 1), "%ud/", id);
+			m->size = ~0UL;
+			m->lines = ~0UL;
+			m->flags = Frecent;
+			if(!msginfo(m) || m->info[Idigest] == 0)
+				freemsg(0, m);
+			else{
+				fstreeadd(box, m);
+				if(last == nil)
+					box->msgs = m;
+				else
+					last->next = m;
+				last = m;
+			}
+		}
+		free(d);
+	}
+
+	/* box->max is invalid here */
+	return new;
+}
+
+int
+uidcmp(void *va, void *vb)
+{
+	Msg **a, **b;
+
+	a = va;
+	b = vb;
+	return (*a)->uid - (*b)->uid;
+}
+
+static void
+sequence(Box *box)
+{
+	Msg **a, *m;
+	int n, i;
+
+	n = 0;
+	for(m = box->msgs; m; m = m->next)
+		n++;
+	a = ezmalloc(n * sizeof *a);
+	i = 0;
+	for(m = box->msgs; m; m = m->next)
+		a[i++] = m;
+	qsort(a, n, sizeof *a, uidcmp);
+	for(i = 0; i < n - 1; i++)
+		a[i]->next = a[i + 1];
+	for(i = 0; i < n; i++)
+		if(a[i]->seq && a[i]->seq != i + 1)
+			bye("internal error assigning message numbers");
+		else
+			a[i]->seq = i + 1;
+	box->msgs = nil;
+	if(n > 0){
+		a[n - 1]->next = nil;
+		box->msgs = a[0];
+	}
+	box->max = n;
+	memset(a, 0, n*sizeof *a);
+	free(a);
+}
+
+/*
+ * strategy:
+ * every mailbox file has an associated .imp file
+ * which maps upas/fs message digests to uids & message flags.
+ *
+ * the .imp files are locked by /mail/fs/usename/L.mbox.
+ * whenever the flags can be modified, the lock file
+ * should be opened, thereby locking the uid & flag state.
+ * for example, whenever new uids are assigned to messages,
+ * and whenever flags are changed internally, the lock file
+ * should be open and locked.  this means the file must be
+ * opened during store command, and when changing the \seen
+ * flag for the fetch command.
+ *
+ * if no .imp file exists, a null one must be created before
+ * assigning uids.
+ *
+ * the .imp file has the following format
+ * imp		: "imap internal mailbox description\n"
+ * 			uidvalidity " " uidnext "\n"
+ *			messagelines
+ *
+ * messagelines	:
+ *		| messagelines digest " " uid " " flags "\n"
+ *
+ * uid, uidnext, and uidvalidity are 32 bit decimal numbers
+ * printed right justified in a field Nuid characters long.
+ * the 0 uid implies that no uid has been assigned to the message,
+ * but the flags are valid. note that message lines are in mailbox
+ * order, except possibly for 0 uid messages.
+ *
+ * digest is an ascii hex string Ndigest characters long.
+ *
+ * flags has a character for each of NFlag flag fields.
+ * if the flag is clear, it is represented by a "-".
+ * set flags are represented as a unique single ascii character.
+ * the currently assigned flags are, in order:
+ *	Fseen		s
+ *	Fanswered	a
+ *	Fflagged	f
+ *	Fdeleted	D
+ *	Fdraft		d
+ */
+
+Box*
+openbox(char *name, char *fsname, int writable)
+{
+	char err[ERRMAX];
+	int new;
+	Box *box;
+	Mblock *ml;
+
+	fsinit();
+if(!strcmp(name, "mbox"))ilog("open %F %q", name, fsname);
+	if(fprint(fsctl, "open %F %q", name, fsname) < 0){
+		rerrstr(err, sizeof err);
+		if(strstr(err, "file does not exist") == nil)
+			ilog("fs open %F as %s: %s", name, fsname, err);
+		if(strstr(err, "hungup channel"))
+			bye(Ecanttalk);
+		fprint(fsctl, "close %s", fsname);
+		return nil;
+	}
+
+	/*
+	 * read box to find all messages
+	 * each one has a directory, and is in numerical order
+	 */
+	box = MKZ(Box);
+	box->writable = writable;
+	box->name = smprint("%s", name);
+	box->imp = smprint("%s.imp", name);
+	box->fs = smprint("%s", fsname);
+	box->fsdir = smprint("/mail/fs/%s", fsname);
+	box->uidnext = 1;
+	box->fstree = avlcreate(fstreecmp);
+	new = readbox(box);
+	if(new >= 0 && (ml = openimp(box, new))){
+		closeimp(box, ml);
+		sequence(box);
+		return box;
+	}
+	closebox(box, 0);
+	return nil;
+}
+
+/*
+ * careful: called by idle polling proc
+ */
+Mblock*
+checkbox(Box *box, int imped)
+{
+	int new;
+	Dir *d;
+	Mblock *ml;
+
+	if(box == nil)
+		return nil;
+
+	/*
+	 * if stat fails, mailbox must be gone
+	 */
+	d = cddirstat(box->fsdir, ".");
+	if(d == nil){
+		mboxgone(box);
+		return nil;
+	}
+	new = 0;
+	if(box->qid.path != d->qid.path || box->qid.vers != d->qid.vers
+	|| box->mtime != d->mtime){
+		new = readbox(box);
+		if(new < 0){
+			free(d);
+			return nil;
+		}
+	}
+	free(d);
+	ml = openimp(box, new);
+	if(ml == nil){
+		ilog("openimp fails; box->writable = 0: %r");
+		box->writable = 0;
+	}else if(!imped){
+		closeimp(box, ml);
+		ml = nil;
+	}
+	if(new || box->dirtyimp)
+		sequence(box);
+	return ml;
+}
+
+/*
+ * close the .imp file, after writing out any changes
+ */
+void
+closeimp(Box *box, Mblock *ml)
+{
+	int fd;
+	Biobuf b;
+	Qid qid;
+
+	if(ml == nil)
+		return;
+	if(!box->dirtyimp){
+		mbunlock(ml);
+		return;
+	}
+	fd = cdcreate(mboxdir, box->imp, OWRITE, 0664);
+	if(fd < 0){
+		mbunlock(ml);
+		return;
+	}
+	Binit(&b, fd, OWRITE);
+	box->dirtyimp = 0;
+	wrimp(&b, box);
+	Bterm(&b);
+
+	if(fqid(fd, &qid) == 0)
+		box->impqid = qid;
+	close(fd);
+	mbunlock(ml);
+}
+
+void
+closebox(Box *box, int opened)
+{
+	Msg *m, *next;
+
+	/*
+	 * make sure to leave the mailbox directory so upas/fs can close the mailbox
+	 */
+	mychdir(mboxdir);
+
+	if(box->writable){
+		deletemsg(box, 0);
+		if(expungemsgs(box, 0))
+			closeimp(box, checkbox(box, 1));
+	}
+
+	if(fprint(fsctl, "close %s", box->fs) < 0 && opened)
+		bye(Ecanttalk);
+	for(m = box->msgs; m != nil; m = next){
+		next = m->next;
+		freemsg(box, m);
+	}
+	free(box->name);
+	free(box->fs);
+	free(box->fsdir);
+	free(box->imp);
+	free(box->fstree);
+	free(box);
+}
+
+int
+deletemsg(Box *box, Msgset *ms)
+{
+	char buf[Bufsize], *p, *start;
+	int ok;
+	Msg *m;
+
+	if(!box->writable)
+		return 0;
+
+	/*
+	 * first pass: delete messages; gang the writes together for speed.
+	 */
+	ok = 1;
+	start = seprint(buf, buf + sizeof buf, "delete %s", box->fs);
+	p = start;
+	for(m = box->msgs; m != nil; m = m->next)
+		if(ms == 0 || ms && inmsgset(ms, m->uid))
+		if((m->flags & Fdeleted) && !m->expunged){
+			m->expunged = 1;
+			p = seprint(p, buf + sizeof buf, " %ud", m->id);
+			if(p + 32 >= buf + sizeof buf){
+				if(write(fsctl, buf, p - buf) == -1)
+					bye(Ecanttalk);
+				p = start;
+			}
+		}
+	if(p != start && write(fsctl, buf, p - buf) == -1)
+		bye(Ecanttalk);
+	return ok;
+}
+
+/*
+ * second pass: remove the message structure,
+ * and renumber message sequence numbers.
+ * update messages counts in mailbox.
+ * returns true if anything changed.
+ */
+int
+expungemsgs(Box *box, int send)
+{
+	uint n;
+	Msg *m, *next, *last;
+
+	n = 0;
+	last = nil;
+	for(m = box->msgs; m != nil; m = next){
+		m->seq -= n;
+		next = m->next;
+		if(m->expunged){
+			if(send)
+				Bprint(&bout, "* %ud expunge\r\n", m->seq);
+			if(m->flags & Frecent)
+				box->recent--;
+			n++;
+			if(last == nil)
+				box->msgs = next;
+			else
+				last->next = next;
+			freemsg(box, m);
+		}else
+			last = m;
+	}
+	if(n){
+		box->max -= n;
+		box->dirtyimp = 1;
+	}
+	return n;
+}
+
+static char *stoplist[] =
+{
+	".",
+	"dead.letter",
+	"forward",
+	"headers",
+	"imap.subscribed",
+	"mbox",
+	"names",
+	"pipefrom",
+	"pipeto",
+	0
+};
+
+/*
+ * reject bad mailboxes based on mailbox name
+ */
+int
+okmbox(char *path)
+{
+	char *name;
+	int i, c;
+
+	name = strrchr(path, '/');
+	if(name == nil)
+		name = path;
+	else
+		name++;
+	if(strlen(name) + STRLEN(".imp") >= Pathlen)
+		return 0;
+	for(i = 0; stoplist[i]; i++)
+		if(strcmp(name, stoplist[i]) == 0)
+			return 0;
+	c = name[0];
+	if(c == 0 || c == '-' || c == '/'
+	|| isdotdot(name)
+	|| isprefix("L.", name)
+	|| isprefix("imap-tmp.", name)
+	|| issuffix("-", name)
+	|| issuffix(".00", name)
+	|| issuffix(".imp", name)
+	|| issuffix(".idx", name))
+		return 0;
+
+	return 1;
+}
+
+int
+creatembox(char *mbox)
+{
+	fsinit();
+	if(fprint(fsctl, "create %q", mbox) > 0){
+		fprint(fsctl, "close %s", mbox);
+		return 0;
+	}
+	return -1;
+}
+
+/*
+ * rename mailbox.  truncaes or removes the source.
+ * bug? is the lock required
+ * upas/fs helpfully moves our .imp file.
+ */
+int
+renamebox(char *from, char *to, int doremove)
+{
+	char *p;
+	int r;
+	Mblock *ml;
+
+	fsinit();
+	ml = mblock();
+	if(ml == nil)
+		return 0;
+	if(doremove)
+		r = fprint(fsctl, "rename %F %F", from, to);
+	else
+		r = fprint(fsctl, "rename -t %F %F", from, to);
+	if(r > 0){
+		if(p = strrchr(to, '/'))
+			p++;
+		else
+			p = to;
+		fprint(fsctl, "close %s", p);
+	}
+	mbunlock(ml);
+	return r > 0;
+}
+
+/*
+ * upas/fs likes us; he removes the .imp file
+ */
+int
+removembox(char *path)
+{
+	fsinit();
+	return fprint(fsctl, "remove %s", path) > 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/mkfile
@@ -1,0 +1,33 @@
+</$objtype/mkfile
+
+TARG=imap4d
+
+OFILES=\
+	auth.$O\
+	copy.$O\
+	csquery.$O\
+	date.$O\
+	debug.$O\
+	fetch.$O\
+	folder.$O\
+	fsenc.$O\
+	fstree.$O\
+	imp.$O\
+	imap4d.$O\
+	list.$O\
+	mbox.$O\
+	msg.$O\
+	mutf7.$O\
+	nodes.$O\
+	print.$O\
+	quota.$O\
+	search.$O\
+	store.$O\
+	utils.$O\
+
+HFILES=\
+	imap4d.h\
+	fns.h\
+
+</sys/src/cmd/mkone
+<../mkupas
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/msg.c
@@ -1,0 +1,1392 @@
+#include "imap4d.h"
+
+static	char	*headaddrspec(char*, char*);
+static	Maddr	*headaddresses(void);
+static	Maddr	*headaddress(void);
+static	char	*headatom(char*);
+static	int	headchar(int eat);
+static	char	*headdomain(char*);
+static	Maddr	*headmaddr(Maddr*);
+static	char	*headphrase(char*, char*);
+static	char	*headquoted(int start, int stop);
+static	char	*headskipwhite(int);
+static	void	headskip(void);
+static	char	*headsubdomain(void);
+static	char	*headtext(void);
+static	void	headtoend(void);
+static	char	*headword(void);
+static	void	mimedescription(Header*);
+static	void	mimedisposition(Header*);
+static	void	mimeencoding(Header*);
+static	void	mimeid(Header*);
+static	void	mimelanguage(Header*);
+//static	void	mimemd5(Header*);
+static	void	mimetype(Header*);
+static	int	msgbodysize(Msg*);
+static	int	msgheader(Msg*, Header*, char*);
+
+/*
+ * stop list for header fields
+ */
+static	char	*headfieldstop = ":";
+static	char	*mimetokenstop = "()<>@,;:\\\"/[]?=";
+static	char	*headatomstop = "()<>@,;:\\\".[]";
+static	uchar	*headstr;
+static	uchar	*lastwhite;
+
+long
+selectfields(char *dst, long n, char *hdr, Slist *fields, int matches)
+{
+	char *s;
+	uchar *start;
+	long m, nf;
+	Slist *f;
+
+	headstr = (uchar*)hdr;
+	m = 0;
+	for(;;){
+		start = headstr;
+		s = headatom(headfieldstop);
+		if(s == nil)
+			break;
+		headskip();
+		for(f = fields; f != nil; f = f->next){
+			if(cistrcmp(s, f->s) == !matches){
+				nf = headstr - start;
+				if(m + nf > n)
+					return 0;
+				memmove(&dst[m], start, nf);
+				m += nf;
+			}
+		}
+		free(s);
+	}
+	if(m + 3 > n)
+		return 0;
+	dst[m++] = '\r';
+	dst[m++] = '\n';
+	dst[m] = '\0';
+	return m;
+}
+
+static Mimehdr*
+mkmimehdr(char *s, char *t, Mimehdr *next)
+{
+	Mimehdr *mh;
+
+	mh = MK(Mimehdr);
+	mh->s = s;
+	mh->t = t;
+	mh->next = next;
+	return mh;
+}
+
+static void
+freemimehdr(Mimehdr *mh)
+{
+	Mimehdr *last;
+
+	while(mh != nil){
+		last = mh;
+		mh = mh->next;
+		free(last->s);
+		free(last->t);
+		free(last);
+	}
+}
+
+static void
+freeheader(Header *h)
+{
+	freemimehdr(h->type);
+	freemimehdr(h->id);
+	freemimehdr(h->description);
+	freemimehdr(h->encoding);
+//	freemimehdr(h->md5);
+	freemimehdr(h->disposition);
+	freemimehdr(h->language);
+	free(h->buf);
+}
+
+static void
+freemaddr(Maddr *a)
+{
+	Maddr *p;
+
+	while(a != nil){
+		p = a;
+		a = a->next;
+		free(p->personal);
+		free(p->box);
+		free(p->host);
+		free(p);
+	}
+}
+
+void
+freemsg(Box *box, Msg *m)
+{
+	Msg *k, *last;
+
+	if(box != nil)
+		fstreedelete(box, m);
+	free(m->ibuf);
+	freemaddr(m->to);
+	if(m->replyto != m->from)
+		freemaddr(m->replyto);
+	if(m->sender != m->from)
+		freemaddr(m->sender);
+	freemaddr(m->from);
+	freemaddr(m->cc);
+	freemaddr(m->bcc);
+	freeheader(&m->head);
+	freeheader(&m->mime);
+	for(k = m->kids; k != nil; ){
+		last = k;
+		k = k->next;
+		freemsg(0, last);
+	}
+	free(m->fs);
+	free(m);
+}
+
+uint
+msgsize(Msg *m)
+{
+	return m->head.size + m->size;
+}
+
+char*
+maddrstr(Maddr *a)
+{
+	char *host, *addr;
+
+	host = a->host;
+	if(host == nil)
+		host = "";
+	if(a->personal != nil)
+		addr = smprint("%s <%s@%s>", a->personal, a->box, host);
+	else
+		addr = smprint("%s@%s", a->box, host);
+	return addr;
+}
+
+int
+msgfile(Msg *m, char *f)
+{
+	if(strlen(f) > Filelen)
+		bye("internal error: msgfile name too long");
+	strcpy(m->efs, f);
+	return cdopen(m->fsdir, m->fs, OREAD);
+}
+
+int
+msgismulti(Header *h)
+{
+	return h->type != nil && cistrcmp("multipart", h->type->s) == 0;
+}
+
+int
+msgis822(Header *h)
+{
+	Mimehdr *t;
+
+	t = h->type;
+	return t != nil && cistrcmp("message", t->s) == 0 && cistrcmp("rfc822", t->t) == 0;
+}
+
+/*
+ * check if a message has been deleted by someone else
+ */
+void
+msgdead(Msg *m)
+{
+	if(m->expunged)
+		return;
+	*m->efs = '\0';
+	if(!cdexists(m->fsdir, m->fs))
+		m->expunged = 1;
+}
+
+static long
+msgreadfile(Msg *m, char *file, char **ss)
+{
+	char *s, buf[Bufsize];
+	int fd;
+	long n, nn;
+	vlong length;
+	Dir *d;
+
+	fd = msgfile(m, file);
+	if(fd < 0){
+		msgdead(m);
+		return -1;
+	}
+
+	n = read(fd, buf, Bufsize);
+	if(n < Bufsize){
+		close(fd);
+		if(n < 0){
+			*ss = nil;
+			return -1;
+		}
+		s = emalloc(n + 1);
+		memmove(s, buf, n);
+		s[n] = '\0';
+		*ss = s;
+		return n;
+	}
+
+	d = dirfstat(fd);
+	if(d == nil){
+		close(fd);
+		return -1;
+	}
+	length = d->length;
+	free(d);
+	nn = length;
+	s = emalloc(nn + 1);
+	memmove(s, buf, n);
+	if(nn > n)
+		nn = readn(fd, s + n, nn - n) + n;
+	close(fd);
+	if(nn != length){
+		free(s);
+		return -1;
+	}
+	s[nn] = '\0';
+	*ss = s;
+	return nn;
+}
+
+/*
+ * make sure the message has valid associated info
+ * used for Isubject, Idigest, Iinreplyto, Imessageid.
+ */
+int
+msginfo(Msg *m)
+{
+	char *s;
+	int i;
+
+	if(m->info[0] != nil)
+		return 1;
+	if(msgreadfile(m, "info", &m->ibuf) < 0)
+		return 0;
+	s = m->ibuf;
+	for(i = 0; i < Imax; i++){
+		m->info[i] = s;
+		s = strchr(s, '\n');
+		if(s == nil)
+			return 0;
+		if(s == m->info[i])
+			m->info[i] = 0;
+		*s++ = '\0';
+	}
+//	m->lines = strtoul(m->info[Ilines], 0, 0);
+//	m->size = strtoull(m->info[Isize], 0, 0);
+//	m->size += m->lines;			/* BOTCH: this hack belongs elsewhere */
+	return 1;
+}
+
+/*
+ * make sure the message has valid mime structure
+ * and sub-messages
+ */
+int
+msgstruct(Msg *m, int top)
+{
+	char buf[12];
+	int fd, ns, max;
+	Msg *k, head, *last;
+
+	if(m->kids != nil)
+		return 1;
+	if(m->expunged
+	|| !msginfo(m)
+	|| !msgheader(m, &m->mime, "mimeheader")){
+		msgdead(m);
+		return 0;
+	}
+	/* gack.  we need to get the header from the subpart here. */
+	if(msgis822(&m->mime)){
+		free(m->ibuf);
+		m->info[0] = 0;
+		m->efs = seprint(m->efs, m->efs + 5, "/1/");
+		if(!msginfo(m)){
+			msgdead(m);
+			return 0;
+		}
+	}
+	if(!msgbodysize(m)
+	|| (top || msgis822(&m->mime) || msgismulti(&m->mime)) && !msgheader(m, &m->head, "rawheader")){
+		msgdead(m);
+		return 0;
+	}
+
+	/*
+	 * if a message has no kids, it has a kid which is just the body of the real message
+	 */
+	if(!msgismulti(&m->head) && !msgismulti(&m->mime) && !msgis822(&m->head) && !msgis822(&m->mime)){
+		k = MKZ(Msg);
+		k->id = 1;
+		k->fsdir = m->fsdir;
+		k->parent = m->parent;
+		ns = m->efs - m->fs;
+		k->fs = emalloc(ns + (Filelen + 1));
+		memmove(k->fs, m->fs, ns);
+		k->efs = k->fs + ns;
+		*k->efs = '\0';
+		k->size = m->size;
+		m->kids = k;
+		return 1;
+	}
+
+	/*
+	 * read in all child messages messages
+	 */
+	head.next = nil;
+	last = &head;
+	for(max = 1;; max++){
+		snprint(buf, sizeof buf, "%d", max);
+		fd = msgfile(m, buf);
+		if(fd == -1)
+			break;
+		close(fd);
+		m->efs[0] = 0;		/* BOTCH! */
+
+		k = MKZ(Msg);
+		k->id = max;
+		k->fsdir = m->fsdir;
+		k->parent = m;
+		ns = strlen(m->fs) + 2*(Filelen + 1);
+		k->fs = emalloc(ns);
+		k->efs = seprint(k->fs, k->fs + ns, "%s%d/", m->fs, max);
+		k->size = ~0UL;
+		k->lines = ~0UL;
+		last->next = k;
+		last = k;
+	}
+
+	m->kids = head.next;
+
+	/*
+	 * if kids fail, just whack them
+	 */
+	top = top && (msgis822(&m->head) || msgismulti(&m->head));
+	for(k = m->kids; k != nil; k = k->next)
+		if(!msgstruct(k, top)){
+			debuglog("kid fail %p %s", k, k->fs);
+			for(k = m->kids; k != nil; ){
+				last = k;
+				k = k->next;
+				freemsg(0, last);
+			}
+			m->kids = nil;
+			break;
+		}
+	return 1;
+}
+
+/*
+ * read in the message body to count \n without a preceding \r
+ */
+static int
+msgbodysize(Msg *m)
+{
+	char buf[Bufsize + 2], *s, *se;
+	uint length, size, lines, needr;
+	int n, fd, c;
+	Dir *d;
+
+	if(m->lines != ~0UL)
+		return 1;
+	fd = msgfile(m, "rawbody");
+	if(fd < 0)
+		return 0;
+	d = dirfstat(fd);
+	if(d == nil){
+		close(fd);
+		return 0;
+	}
+	length = d->length;
+	free(d);
+
+	size = 0;
+	lines = 0;
+	needr = 0;
+	buf[0] = ' ';
+	for(;;){
+		n = read(fd, &buf[1], Bufsize);
+		if(n <= 0)
+			break;
+		size += n;
+		se = &buf[n + 1];
+		for(s = &buf[1]; s < se; s++){
+			c = *s;
+			if(c == '\0')
+				*s = ' ';
+			if(c != '\n')
+				continue;
+			if(s[-1] != '\r')
+				needr++;
+			lines++;
+		}
+		buf[0] = buf[n];
+	}
+	if(size != length)
+		bye("bad length reading rawbody %d != %d; n %d %s", size, length, n, m->fs);
+	size += needr;
+	m->size = size;
+	m->lines = lines;
+	close(fd);
+	return 1;
+}
+
+/*
+ * prepend hdrname: val to the cached header
+ */
+static void
+msgaddhead(Msg *m, char *hdrname, char *val)
+{
+	char *s;
+	long size, n;
+
+	n = strlen(hdrname) + strlen(val) + 4;
+	size = m->head.size + n;
+	s = emalloc(size + 1);
+	snprint(s, size + 1, "%s: %s\r\n%s", hdrname, val, m->head.buf);
+	free(m->head.buf);
+	m->head.buf = s;
+	m->head.size = size;
+	m->head.lines++;
+}
+
+static void
+msgadddate(Msg *m)
+{
+	char buf[64];
+	Tm tm;
+
+	/* don't bother if we don't have a date */
+	if(m->info[Idate] == 0)
+		return;
+
+	date2tm(&tm, m->info[Idate]);
+	snprint(buf, sizeof buf, "%δ", &tm);
+	msgaddhead(m, "Date", buf);
+}
+
+/*
+ * read in the entire header,
+ * and parse out any existing mime headers
+ */
+static int
+msgheader(Msg *m, Header *h, char *file)
+{
+	char *s, *ss, *t, *te;
+	int dated, c;
+	long ns;
+	uint lines, n, nn;
+
+	if(h->buf != nil)
+		return 1;
+
+	ns = msgreadfile(m, file, &ss);
+	if(ns < 0)
+		return 0;
+	s = ss;
+	n = ns;
+
+	/*
+	 * count lines ending with \n and \r\n
+	 * add an extra line at the end, since upas/fs headers
+	 * don't have a terminating \r\n
+	 */
+	lines = 1;
+	te = s + ns;
+	for(t = s; t < te; t++){
+		c = *t;
+		if(c == '\0')
+			*t = ' ';
+		if(c != '\n')
+			continue;
+		if(t == s || t[-1] != '\r')
+			n++;
+		lines++;
+	}
+	if(t > s && t[-1] != '\n'){
+		if(t[-1] != '\r')
+			n++;
+		n++;
+	}
+	if(n > 0)
+		n += 2;
+	h->buf = emalloc(n + 1);
+	h->size = n;
+	h->lines = lines;
+
+	/*
+	 * make sure all headers end in \r\n
+	 */
+	nn = 0;
+	for(t = s; t < te; t++){
+		c = *t;
+		if(c == '\n'){
+			if(!nn || h->buf[nn - 1] != '\r')
+				h->buf[nn++] = '\r';
+			lines++;
+		}
+		h->buf[nn++] = c;
+	}
+	if(nn && h->buf[nn-1] != '\n'){
+		if(h->buf[nn-1] != '\r')
+			h->buf[nn++] = '\r';
+		h->buf[nn++] = '\n';
+	}
+	if(nn > 0){
+		h->buf[nn++] = '\r';
+		h->buf[nn++] = '\n';
+	}
+	h->buf[nn] = '\0';
+	if(nn != n)
+		bye("misconverted header %d %d", nn, n);
+	free(s);
+
+	/*
+	 * and parse some mime headers
+	 */
+	headstr = (uchar*)h->buf;
+	dated = 0;
+	while(s = headatom(headfieldstop)){
+		if(cistrcmp(s, "content-type") == 0)
+			mimetype(h);
+		else if(cistrcmp(s, "content-transfer-encoding") == 0)
+			mimeencoding(h);
+		else if(cistrcmp(s, "content-id") == 0)
+			mimeid(h);
+		else if(cistrcmp(s, "content-description") == 0)
+			mimedescription(h);
+		else if(cistrcmp(s, "content-disposition") == 0)
+			mimedisposition(h);
+//		else if(cistrcmp(s, "content-md5") == 0)
+//			mimemd5(h);
+		else if(cistrcmp(s, "content-language") == 0)
+			mimelanguage(h);
+		else if(h == &m->head){
+			if(cistrcmp(s, "from") == 0)
+				m->from = headmaddr(m->from);
+			else if(cistrcmp(s, "to") == 0)
+				m->to = headmaddr(m->to);
+			else if(cistrcmp(s, "reply-to") == 0)
+				m->replyto = headmaddr(m->replyto);
+			else if(cistrcmp(s, "sender") == 0)
+				m->sender = headmaddr(m->sender);
+			else if(cistrcmp(s, "cc") == 0)
+				m->cc = headmaddr(m->cc);
+			else if(cistrcmp(s, "bcc") == 0)
+				m->bcc = headmaddr(m->bcc);
+			else if(cistrcmp(s, "date") == 0)
+				dated = 1;
+		}
+		headskip();
+		free(s);
+	}
+
+	if(h == &m->head){
+		if(m->sender == nil)
+			m->sender = m->from;
+		if(m->replyto == nil)
+			m->replyto = m->from;
+		if(!dated && m->from != nil)
+			msgadddate(m);
+	}
+	return 1;
+}
+
+/*
+ * q is a quoted string.  remove enclosing " and and \ escapes
+ */
+static void
+stripquotes(char *q)
+{
+	char *s;
+	int c;
+
+	if(q == nil)
+		return;
+	s = q++;
+	while(c = *q++){
+		if(c == '\\'){
+			c = *q++;
+			if(!c)
+				return;
+		}
+		*s++ = c;
+	}
+	s[-1] = '\0';
+}
+
+/*
+ * parser for rfc822 & mime header fields
+ */
+
+/*
+ * params	:
+ *		| params ';' token '=' token
+ * 		| params ';' token '=' quoted-str
+ */
+static Mimehdr*
+mimeparams(void)
+{
+	char *s, *t;
+	Mimehdr head, *last;
+
+	head.next = nil;
+	last = &head;
+	for(;;){
+		if(headchar(1) != ';')
+			break;
+		s = headatom(mimetokenstop);
+		if(s == nil || headchar(1) != '='){
+			free(s);
+			break;
+		}
+		if(headchar(0) == '"'){
+			t = headquoted('"', '"');
+			stripquotes(t);
+		}else
+			t = headatom(mimetokenstop);
+		if(t == nil){
+			free(s);
+			break;
+		}
+		last->next = mkmimehdr(s, t, nil);
+		last = last->next;
+	}
+	return head.next;
+}
+
+/*
+ * type		: 'content-type' ':' token '/' token params
+ */
+static void
+mimetype(Header *h)
+{
+	char *s, *t;
+
+	if(headchar(1) != ':')
+		return;
+	s = headatom(mimetokenstop);
+	if(s == nil || headchar(1) != '/'){
+		free(s);
+		return;
+	}
+	t = headatom(mimetokenstop);
+	if(t == nil){
+		free(s);
+		return;
+	}
+	h->type = mkmimehdr(s, t, mimeparams());
+}
+
+/*
+ * encoding	: 'content-transfer-encoding' ':' token
+ */
+static void
+mimeencoding(Header *h)
+{
+	char *s;
+
+	if(headchar(1) != ':')
+		return;
+	s = headatom(mimetokenstop);
+	if(s == nil)
+		return;
+	h->encoding = mkmimehdr(s, nil, nil);
+}
+
+/*
+ * mailaddr	: ':' addresses
+ */
+static Maddr*
+headmaddr(Maddr *old)
+{
+	Maddr *a;
+
+	if(headchar(1) != ':')
+		return old;
+
+	if(headchar(0) == '\n')
+		return old;
+
+	a = headaddresses();
+	if(a == nil)
+		return old;
+
+	freemaddr(old);
+	return a;
+}
+
+/*
+ * addresses	: address | addresses ',' address
+ */
+static Maddr*
+headaddresses(void)
+{
+	Maddr *addr, *tail, *a;
+
+	addr = headaddress();
+	if(addr == nil)
+		return nil;
+	tail = addr;
+	while(headchar(0) == ','){
+		headchar(1);
+		a = headaddress();
+		if(a == nil){
+			freemaddr(addr);
+			return nil;
+		}
+		tail->next = a;
+		tail = a;
+	}
+	return addr;
+}
+
+/*
+ * address	: mailbox | group
+ * group	: phrase ':' mboxes ';' | phrase ':' ';'
+ * mailbox	: addr-spec
+ *		| optphrase '<' addr-spec '>'
+ *		| optphrase '<' route ':' addr-spec '>'
+ * optphrase	: | phrase
+ * route	: '@' domain
+ *		| route ',' '@' domain
+ * personal names are the phrase before '<',
+ * or a comment before or after a simple addr-spec
+ */
+static Maddr*
+headaddress(void)
+{
+	char *s, *e, *w, *personal;
+	uchar *hs;
+	int c;
+	Maddr *addr;
+
+	s = emalloc(strlen((char*)headstr) + 2);
+	e = s;
+	personal = headskipwhite(1);
+	c = headchar(0);
+	if(c == '<')
+		w = nil;
+	else{
+		w = headword();
+		c = headchar(0);
+	}
+	if(c == '.' || c == '@' || c == ',' || c == '\n' || c == '\0'){
+		lastwhite = headstr;
+		e = headaddrspec(s, w);
+		if(personal == nil){
+			hs = headstr;
+			headstr = lastwhite;
+			personal = headskipwhite(1);
+			headstr = hs;
+		}
+	}else{
+		if(c != '<' || w != nil){
+			free(personal);
+			if(!headphrase(e, w)){
+				free(s);
+				return nil;
+			}
+
+			/*
+			 * ignore addresses with groups,
+			 * so the only thing left if <
+			 */
+			c = headchar(1);
+			if(c != '<'){
+				free(s);
+				return nil;
+			}
+			personal = estrdup(s);
+		}else
+			headchar(1);
+
+		/*
+		 * after this point, we need to free personal before returning.
+		 * set e to nil to everything afterwards fails.
+		 *
+		 * ignore routes, they are useless, and heavily discouraged in rfc1123.
+		 * imap4 reports them up to, but not including, the terminating :
+		 */
+		e = s;
+		c = headchar(0);
+		if(c == '@'){
+			for(;;){
+				c = headchar(1);
+				if(c != '@'){
+					e = nil;
+					break;
+				}
+				headdomain(e);
+				c = headchar(1);
+				if(c != ','){
+					e = s;
+					break;
+				}
+			}
+			if(c != ':')
+				e = nil;
+		}
+
+		if(e != nil)
+			e = headaddrspec(s, nil);
+		if(headchar(1) != '>')
+			e = nil;
+	}
+
+	/*
+	 * e points to @host, or nil if an error occured
+	 */
+	if(e == nil){
+		free(personal);
+		addr = nil;
+	}else{
+		if(*e != '\0')
+			*e++ = '\0';
+		else
+			e = site;
+		addr = MKZ(Maddr);
+
+		addr->personal = personal;
+		addr->box = estrdup(s);
+		addr->host = estrdup(e);
+	}
+	free(s);
+	return addr;
+}
+
+/*
+ * phrase	: word
+ *		| phrase word
+ * w is the optional initial word of the phrase
+ * returns the end of the phrase, or nil if a failure occured
+ */
+static char*
+headphrase(char *e, char *w)
+{
+	int c;
+
+	for(;;){
+		if(w == nil){
+			w = headword();
+			if(w == nil)
+				return nil;
+		}
+		if(w[0] == '"')
+			stripquotes(w);
+		strcpy(e, w);
+		free(w);
+		w = nil;
+		e = strchr(e, '\0');
+		c = headchar(0);
+		if(c <= ' ' || strchr(headatomstop, c) != nil && c != '"')
+			break;
+		*e++ = ' ';
+		*e = '\0';
+	}
+	return e;
+}
+
+/*
+ * find the ! in domain!rest, where domain must have at least
+ * one internal '.'
+ */
+static char*
+dombang(char *s)
+{
+	int dot, c;
+
+	dot = 0;
+	for(; c = *s; s++){
+		if(c == '!'){
+			if(!dot || dot == 1 && s[-1] == '.' || s[1] == '\0')
+				return nil;
+			return s;
+		}
+		if(c == '"')
+			break;
+		if(c == '.')
+			dot++;
+	}
+	return nil;
+}
+
+/*
+ * addr-spec	: local-part '@' domain
+ *		| local-part			extension to allow ! and local names
+ * local-part	: word
+ *		| local-part '.' word
+ *
+ * if no '@' is present, rewrite d!e!f!u as @d,@e:u@f,
+ * where d, e, f are valid domain components.
+ * the @d,@e: is ignored, since routes are ignored.
+ * perhaps they should be rewritten as e!f!u@d, but that is inconsistent with upas.
+ *
+ * returns a pointer to '@', the end if none, or nil if there was an error
+ */
+static char*
+headaddrspec(char *e, char *w)
+{
+	char *s, *at, *b, *bang, *dom;
+	int c;
+
+	s = e;
+	for(;;){
+		if(w == nil){
+			w = headword();
+			if(w == nil)
+				return nil;
+		}
+		strcpy(e, w);
+		free(w);
+		w = nil;
+		e = strchr(e, '\0');
+		lastwhite = headstr;
+		c = headchar(0);
+		if(c != '.')
+			break;
+		headchar(1);
+		*e++ = '.';
+		*e = '\0';
+	}
+
+	if(c != '@'){
+		/*
+		 * extenstion: allow name without domain
+		 * check for domain!xxx
+		 */
+		bang = dombang(s);
+		if(bang == nil)
+			return e;
+
+		/*
+		 * if dom1!dom2!xxx, ignore dom1!
+		 */
+		dom = s;
+		for(; b = dombang(bang + 1); bang = b)
+			dom = bang + 1;
+
+		/*
+		 * convert dom!mbox into mbox@dom
+		 */
+		*bang = '@';
+		strrev(dom, bang);
+		strrev(bang + 1, e);
+		strrev(dom, e);
+		bang = &dom[e - bang - 1];
+		if(dom > s){
+			bang -= dom - s;
+			for(e = s; *e = *dom; e++)
+				dom++;
+		}
+
+		/*
+		 * eliminate a trailing '.'
+		 */
+		if(e[-1] == '.')
+			e[-1] = '\0';
+		return bang;
+	}
+	headchar(1);
+
+	at = e;
+	*e++ = '@';
+	*e = '\0';
+	if(!headdomain(e))
+		return nil;
+	return at;
+}
+
+/*
+ * domain	: sub-domain
+ *		| domain '.' sub-domain
+ * returns the end of the domain, or nil if a failure occured
+ */
+static char*
+headdomain(char *e)
+{
+	char *w;
+
+	for(;;){
+		w = headsubdomain();
+		if(w == nil)
+			return nil;
+		strcpy(e, w);
+		free(w);
+		e = strchr(e, '\0');
+		lastwhite = headstr;
+		if(headchar(0) != '.')
+			break;
+		headchar(1);
+		*e++ = '.';
+		*e = '\0';
+	}
+	return e;
+}
+
+/*
+ * id		: 'content-id' ':' msg-id
+ * msg-id	: '<' addr-spec '>'
+ */
+static void
+mimeid(Header *h)
+{
+	char *s, *e, *w;
+
+	if(headchar(1) != ':')
+		return;
+	if(headchar(1) != '<')
+		return;
+
+	s = emalloc(strlen((char*)headstr) + 3);
+	e = s;
+	*e++ = '<';
+	e = headaddrspec(e, nil);
+	if(e == nil || headchar(1) != '>'){
+		free(s);
+		return;
+	}
+	e = strchr(e, '\0');
+	*e++ = '>';
+	e[0] = '\0';
+	w = strdup(s);
+	free(s);
+	h->id = mkmimehdr(w, nil, nil);
+}
+
+/*
+ * description	: 'content-description' ':' *text
+ */
+static void
+mimedescription(Header *h)
+{
+	if(headchar(1) != ':')
+		return;
+	headskipwhite(0);
+	h->description = mkmimehdr(headtext(), nil, nil);
+}
+
+/*
+ * disposition	: 'content-disposition' ':' token params
+ */
+static void
+mimedisposition(Header *h)
+{
+	char *s;
+
+	if(headchar(1) != ':')
+		return;
+	s = headatom(mimetokenstop);
+	if(s == nil)
+		return;
+	h->disposition = mkmimehdr(s, nil, mimeparams());
+}
+
+/*
+ * md5		: 'content-md5' ':' token
+ */
+//static void
+//mimemd5(Header *h)
+//{
+//	char *s;
+//
+//	if(headchar(1) != ':')
+//		return;
+//	s = headatom(mimetokenstop);
+//	if(s == nil)
+//		return;
+//	h->md5 = mkmimehdr(s, nil, nil);
+//}
+
+/*
+ * language	: 'content-language' ':' langs
+ * langs	: token
+ *		| langs commas token
+ * commas	: ','
+ *		| commas ','
+ */
+static void
+mimelanguage(Header *h)
+{
+	char *s;
+	Mimehdr head, *last;
+
+	head.next = nil;
+	last = &head;
+	for(;;){
+		s = headatom(mimetokenstop);
+		if(s == nil)
+			break;
+		last->next = mkmimehdr(s, nil, nil);
+		last = last->next;
+		while(headchar(0) != ',')
+			headchar(1);
+	}
+	h->language = head.next;
+}
+
+/*
+ * token	: 1*<char 33-255, except "()<>@,;:\\\"/[]?=" aka mimetokenstop>
+ * atom		: 1*<chars 33-255, except "()<>@,;:\\\".[]" aka headatomstop>
+ * note this allows 8 bit characters, which occur in utf.
+ */
+static char*
+headatom(char *disallowed)
+{
+	char *s;
+	int c, ns, as;
+
+	headskipwhite(0);
+
+	s = emalloc(Stralloc);
+	as = Stralloc;
+	ns = 0;
+	for(;;){
+		c = *headstr++;
+		if(c <= ' ' || strchr(disallowed, c) != nil){
+			headstr--;
+			break;
+		}
+		s[ns++] = c;
+		if(ns >= as){
+			as += Stralloc;
+			s = erealloc(s, as);
+		}
+	}
+	if(ns == 0){
+		free(s);
+		return 0;
+	}
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * sub-domain	: atom | domain-lit
+ */
+static char *
+headsubdomain(void)
+{
+	if(headchar(0) == '[')
+		return headquoted('[', ']');
+	return headatom(headatomstop);
+}
+
+/*
+ * word	: atom | quoted-str
+ */
+static char *
+headword(void)
+{
+	if(headchar(0) == '"')
+		return headquoted('"', '"');
+	return headatom(headatomstop);
+}
+
+/*
+ * quoted-str	: '"' *(any char but '"\\\r', or '\' any char, or linear-white-space) '"'
+ * domain-lit	: '[' *(any char but '[]\\\r', or '\' any char, or linear-white-space) ']'
+ */
+static char *
+headquoted(int start, int stop)
+{
+	char *s;
+	int c, ns, as;
+
+	if(headchar(1) != start)
+		return nil;
+	s = emalloc(Stralloc);
+	as = Stralloc;
+	ns = 0;
+	s[ns++] = start;
+	for(;;){
+		c = *headstr;
+		if(c == stop){
+			headstr++;
+			break;
+		}
+		if(c == '\0'){
+			free(s);
+			return nil;
+		}
+		if(c == '\r'){
+			headstr++;
+			continue;
+		}
+		if(c == '\n'){
+			headstr++;
+			while(*headstr == ' ' || *headstr == '\t' || *headstr == '\r' || *headstr == '\n')
+				headstr++;
+			c = ' ';
+		}else if(c == '\\'){
+			headstr++;
+			s[ns++] = c;
+			c = *headstr;
+			if(c == '\0'){
+				free(s);
+				return nil;
+			}
+			headstr++;
+		}else
+			headstr++;
+		s[ns++] = c;
+		if(ns + 1 >= as){	/* leave room for \c or "0 */
+			as += Stralloc;
+			s = erealloc(s, as);
+		}
+	}
+	s[ns++] = stop;
+	s[ns] = '\0';
+	return s;
+}
+
+/*
+ * headtext	: contents of rest of header line
+ */
+static char *
+headtext(void)
+{
+	uchar *v;
+	char *s;
+
+	v = headstr;
+	headtoend();
+	s = emalloc(headstr - v + 1);
+	memmove(s, v, headstr - v);
+	s[headstr - v] = '\0';
+	return s;
+}
+
+/*
+ * white space is ' ' '\t' or nested comments.
+ * skip white space.
+ * if com and a comment is seen,
+ * return it's contents and stop processing white space.
+ */
+static char*
+headskipwhite(int com)
+{
+	char *s;
+	int c, incom, as, ns;
+
+	s = nil;
+	as = Stralloc;
+	ns = 0;
+	if(com)
+		s = emalloc(Stralloc);
+	incom = 0;
+	for(; c = *headstr; headstr++){
+		switch(c){
+		case ' ':
+		case '\t':
+		case '\r':
+			c = ' ';
+			break;
+		case '\n':
+			c = headstr[1];
+			if(c != ' ' && c != '\t')
+				goto done;
+			c = ' ';
+			break;
+		case '\\':
+			if(com && incom)
+				s[ns++] = c;
+			c = headstr[1];
+			if(c == '\0')
+				goto done;
+			headstr++;
+			break;
+		case '(':
+			incom++;
+			if(incom == 1)
+				continue;
+			break;
+		case ')':
+			incom--;
+			if(com && !incom){
+				s[ns] = '\0';
+				return s;
+			}
+			break;
+		default:
+			if(!incom)
+				goto done;
+			break;
+		}
+		if(com && incom && (c != ' ' || ns > 0 && s[ns-1] != ' ')){
+			s[ns++] = c;
+			if(ns + 1 >= as){	/* leave room for \c or 0 */
+				as += Stralloc;
+				s = erealloc(s, as);
+			}
+		}
+	}
+done:
+	free(s);
+	return nil;
+}
+
+/*
+ * return the next non-white character
+ */
+static int
+headchar(int eat)
+{
+	int c;
+
+	headskipwhite(0);
+	c = *headstr;
+	if(eat && c != '\0' && c != '\n')
+		headstr++;
+	return c;
+}
+
+static void
+headtoend(void)
+{
+	uchar *s;
+	int c;
+
+	for(;;){
+		s = headstr;
+		c = *s++;
+		while(c == '\r')
+			c = *s++;
+		if(c == '\n'){
+			c = *s++;
+			if(c != ' ' && c != '\t')
+				return;
+		}
+		if(c == '\0')
+			return;
+		headstr = s;
+	}
+}
+
+static void
+headskip(void)
+{
+	int c;
+
+	while(c = *headstr){
+		headstr++;
+		if(c == '\n'){
+			c = *headstr;
+			if(c == ' ' || c == '\t')
+				continue;
+			return;
+		}
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/mutf7.c
@@ -1,0 +1,174 @@
+#include "imap4d.h"
+
+/* not compatable with characters outside the basic plane */
+
+/*
+ * modified utf-7, as per imap4 spec
+ * like utf-7, but substitues , for / in base 64,
+ * does not allow escaped ascii characters.
+ *
+ * /lib/rfc/rfc2152 is utf-7
+ * /lib/rfc/rfc1642 is obsolete utf-7
+ *
+ * test sequences from rfc1642
+ *	'A≢Α.'		'A&ImIDkQ-.'
+ *	'Hi Mom ☺!"	'Hi Mom &Jjo-!'
+ *	'日本語'		'&ZeVnLIqe-'
+ */
+
+static uchar mt64d[256];
+static char mt64e[64];
+
+static void
+initm64(void)
+{
+	int c, i;
+
+	memset(mt64d, 255, 256);
+	memset(mt64e, '=', 64);
+	i = 0;
+	for(c = 'A'; c <= 'Z'; c++){
+		mt64e[i] = c;
+		mt64d[c] = i++;
+	}
+	for(c = 'a'; c <= 'z'; c++){
+		mt64e[i] = c;
+		mt64d[c] = i++;
+	}
+	for(c = '0'; c <= '9'; c++){
+		mt64e[i] = c;
+		mt64d[c] = i++;
+	}
+	mt64e[i] = '+';
+	mt64d['+'] = i++;
+	mt64e[i] = ',';
+	mt64d[','] = i;
+}
+
+char*
+encmutf7(char *out, int lim, char *in)
+{
+	char *start, *e;
+	int nb;
+	ulong r, b;
+	Rune rr;
+
+	start = out;
+	e = out + lim;
+	if(mt64e[0] == 0)
+		initm64();
+	if(in)
+	for(;;){
+		r = *(uchar*)in;
+
+		if(r < ' ' || r >= Runeself){
+			if(r == 0)
+				break;
+			if(out + 1 >= e)
+				return 0;
+			*out++ = '&';
+			b = 0;
+			nb = 0;
+			for(;;){
+				in += chartorune(&rr, in);
+				r = rr;
+				if(r == 0 || r >= ' ' && r < Runeself)
+					break;
+				b = (b << 16) | r;
+				for(nb += 16; nb >= 6; nb -= 6){
+					if(out + 1 >= e)
+						return 0;
+					*out++ = mt64e[(b >> nb - 6) & 0x3f];
+				}
+			}
+			for(; nb >= 6; nb -= 6){
+				if(out + 1 >= e)
+					return 0;
+				*out++ = mt64e[(b >> nb - 6) & 0x3f];
+			}
+			if(nb){
+				if(out + 1 >= e)
+					return 0;
+				*out++ = mt64e[(b << 6 - nb) & 0x3f];
+			}
+
+			if(out + 1 >= e)
+				return 0;
+			*out++ = '-';
+			if(r == 0)
+				break;
+		}else
+			in++;
+		if(out + 1 >= e)
+			return 0;
+		*out = r;
+		out++;
+		if(r == '&')
+			*out++ = '-';
+	}
+	*out = 0;
+	if(!in || out >= e)
+		return 0;
+	return start;
+}
+
+char*
+decmutf7(char *out, int lim, char *in)
+{
+	char *start, *e;
+	int c, b, nb;
+	Rune rr;
+
+	start = out;
+	e = out + lim;
+	if(mt64e[0] == 0)
+		initm64();
+	if(in)
+	for(;;){
+		c = *in;
+
+		if(c < ' ' || c >= Runeself){
+			if(c == 0)
+				break;
+			return 0;
+		}
+		if(c != '&'){
+			if(out + 1 >= e)
+				return 0;
+			*out++ = c;
+			in++;
+			continue;
+		}
+		in++;
+		if(*in == '-'){
+			if(out + 1 >= e)
+				return 0;
+			*out++ = '&';
+			in++;
+			continue;
+		}
+
+		b = 0;
+		nb = 0;
+		while((c = *in++) != '-'){
+			c = mt64d[c];
+			if(c >= 64)
+				return 0;
+			b = (b << 6) | c;
+			nb += 6;
+			if(nb >= 16){
+				rr = b >> (nb - 16);
+				nb -= 16;
+				if(out + UTFmax + 1 >= e && out + runelen(rr) + 1 >= e)
+					return 0;
+				out += runetochar(out, &rr);
+			}
+		}
+		if(b & ((1 << nb) - 1))
+			return 0;
+	}
+	*out = 0;
+	if(!in || out >= e)
+		return 0;
+	return start;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/nlisttst.c
@@ -1,0 +1,92 @@
+#include "nlist.c"
+
+Biobuf	bout;
+Bin	*parsebin;
+
+void
+bye(char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	Bprint(&bout, "* bye ");
+	Bvprint(&bout, fmt, arg);
+	Bprint(&bout, "\r\n");
+	Bflush(&bout);
+	exits(0);
+}
+
+static char *stoplist[] =
+{
+	".",
+	"dead.letter",
+	"forward",
+	"headers",
+	"imap.subscribed",
+	"mbox",
+	"names",
+	"pipefrom",
+	"pipeto",
+	0
+};
+int
+okmbox(char *path)
+{
+	char *name;
+	int i, c;
+
+	name = strrchr(path, '/');
+	if(name == nil)
+		name = path;
+	else
+		name++;
+	if(strlen(name) + STRLEN(".imp") >= Pathlen)
+		return 0;
+	for(i = 0; stoplist[i]; i++)
+		if(strcmp(name, stoplist[i]) == 0)
+			return 0;
+	c = name[0];
+	if(c == 0 || c == '-' || c == '/'
+	|| isdotdot(name)
+	|| isprefix("L.", name)
+	|| isprefix("imap-tmp.", name)
+	|| issuffix("-", name)
+	|| issuffix(".00", name)
+	|| issuffix(".imp", name)
+	|| issuffix(".idx", name))
+		return 0;
+
+	return 1;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: nlist ref pat\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int lsub;
+
+	lsub = 0;
+	ARGBEGIN{
+	case 'l':
+		lsub = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+	if(argc != 2)
+		usage();
+	Binit(&bout, 1, OWRITE);
+	quotefmtinstall();
+	if(lsub)
+		Bprint(&bout, "lsub→%d\n", lsubboxes("lsub", argv[0], argv[1]));
+	else
+		Bprint(&bout, "→%d\n", listboxes("list", argv[0], argv[1]));
+	Bterm(&bout);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/nodes.c
@@ -1,0 +1,184 @@
+#include "imap4d.h"
+
+int
+inmsgset(Msgset *ms, uint id)
+{
+	for(; ms; ms = ms->next)
+		if(ms->from <= id && ms->to >= id)
+			return 1;
+	return 0;
+}
+
+/*
+ * we can't rely on uids being in order, but short-circuting saves us
+ * very little.  we have a few tens of thousands of messages at most.
+ * also use the msg list as the outer loop to avoid 1:5,3:7 returning
+ * duplicates.  this is allowed, but silly.  and could be a problem for
+ * internal uses that aren't idempotent, like (re)moving messages.
+ */
+static int
+formsgsu(Box *box, Msgset *s, uint max, int (*f)(Box*, Msg*, int, void*), void *rock)
+{
+	int ok;
+	Msg *m;
+	Msgset *ms;
+
+	ok = 1;
+	for(m = box->msgs; m != nil && m->seq <= max; m = m->next)
+		for(ms = s; ms != nil; ms = ms->next)
+			if(m->uid >= ms->from && m->uid <= ms->to){
+				if(!f(box, m, 1, rock))
+					ok = 0;
+				break;
+			}
+	return ok;
+}
+
+int
+formsgsi(Box *box, Msgset *ms, uint max, int (*f)(Box*, Msg*, int, void*), void *rock)
+{
+	int ok, rok;
+	uint id;
+	Msg *m;
+
+	ok = 1;
+	for(; ms != nil; ms = ms->next){
+		id = ms->from;
+		rok = 0;
+		for(m = box->msgs; m != nil && m->seq <= max; m = m->next){
+			if(m->seq > id)
+				break;	/* optimization */
+			if(m->seq == id){
+				if(!f(box, m, 0, rock))
+					ok = 0;
+				if(id >= ms->to){
+					rok = 1;
+					break;	/* optimization */
+				}
+				if(ms->to == ~0UL)
+					rok = 1;
+				id++;
+			}
+		}
+		if(!rok)
+			ok = 0;
+	}
+	return ok;
+}
+
+/*
+ * iterated over all of the items in the message set.
+ * errors are accumulated, but processing continues.
+ * if uids, then ignore non-existent messages.
+ * otherwise, that's an error.  additional note from the
+ * rfc:
+ *
+ * “Servers MAY coalesce overlaps and/or execute the
+ * sequence in any order.”
+ */
+int
+formsgs(Box *box, Msgset *ms, uint max, int uids, int (*f)(Box*, Msg*, int, void*), void *rock)
+{
+	if(uids)
+		return formsgsu(box, ms, max, f, rock);
+	else
+		return formsgsi(box, ms, max, f, rock);
+}
+
+Store*
+mkstore(int sign, int op, int flags)
+{
+	Store *st;
+
+	st = binalloc(&parsebin, sizeof *st, 1);
+	if(st == nil)
+		parseerr("out of memory");
+	st->sign = sign;
+	st->op = op;
+	st->flags = flags;
+	return st;
+}
+
+Fetch *
+mkfetch(int op, Fetch *next)
+{
+	Fetch *f;
+
+	f = binalloc(&parsebin, sizeof *f, 1);
+	if(f == nil)
+		parseerr("out of memory");
+	f->op = op;
+	f->next = next;
+	return f;
+}
+
+Fetch*
+revfetch(Fetch *f)
+{
+	Fetch *last, *next;
+
+	last = nil;
+	for(; f != nil; f = next){
+		next = f->next;
+		f->next = last;
+		last = f;
+	}
+	return last;
+}
+
+Slist*
+mkslist(char *s, Slist *next)
+{
+	Slist *sl;
+
+	sl = binalloc(&parsebin, sizeof *sl, 0);
+	if(sl == nil)
+		parseerr("out of memory");
+	sl->s = s;
+	sl->next = next;
+	return sl;
+}
+
+Slist*
+revslist(Slist *sl)
+{
+	Slist *last, *next;
+
+	last = nil;
+	for(; sl != nil; sl = next){
+		next = sl->next;
+		sl->next = last;
+		last = sl;
+	}
+	return last;
+}
+
+int
+Bnlist(Biobuf *b, Nlist *nl, char *sep)
+{
+	char *s;
+	int n;
+
+	s = "";
+	n = 0;
+	for(; nl != nil; nl = nl->next){
+		n += Bprint(b, "%s%ud", s, nl->n);
+		s = sep;
+	}
+	return n;
+}
+
+int
+Bslist(Biobuf *b, Slist *sl, char *sep)
+{
+	char *s;
+	int n;
+
+	s = "";
+	n = 0;
+	for(; sl != nil; sl = sl->next){
+		n += Bprint(b, "%s%Z", s, sl->s);
+		s = sep;
+	}
+	return n;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/print.c
@@ -1,0 +1,114 @@
+#include "imap4d.h"
+
+int
+Ffmt(Fmt *f)
+{
+	char *s, buf[128], buf2[128];
+
+	s = va_arg(f->args, char*);
+	if(strncmp("/imap", s, 5) && strncmp("/pop", s, 4)){
+		snprint(buf, sizeof buf, "/mail/box/%s/%s", username, s);
+		s = buf;
+	}
+	snprint(buf2, sizeof buf2, "%q", s);
+	return fmtstrcpy(f, buf2);
+}
+
+enum {
+	Qok		= 0,
+	Qquote		= 1<<0,
+	Qbackslash	= 1<<1,
+	Qliteral		= 1<<2,
+};
+
+static int
+needtoquote(Rune r)
+{
+	if(r >= 0x7f || r == '\n' || r == '\r')
+		return Qliteral;
+	if(r <= ' ')
+		return Qquote;
+	if(r == '\\' || r == '"')
+		return Qbackslash;
+	return Qok;
+}
+
+int
+Zfmt(Fmt *f)
+{
+	char *s, *t, buf[Pathlen], buf2[Pathlen];
+	int w, quotes, alt;
+	Rune r;
+
+	s = va_arg(f->args, char*);
+	alt = f->flags & FmtSharp;
+	if(s == 0 && !alt)
+		return fmtstrcpy(f, "NIL");
+	if(s == 0 || *s == 0)
+		return fmtstrcpy(f, "\"\"");
+	switch(f->r){
+	case 'Y':
+		s = decfs(buf, sizeof buf, s);
+		s = encmutf7(buf2, sizeof buf2, s);
+		break;
+	}
+	quotes = 0;
+	for(t = s; *t; t += w){
+		w = chartorune(&r, t);
+		quotes |= needtoquote(r);
+		if(quotes & Qliteral && alt)
+			ilog("[%s] bad at [%s] %.2ux\n", s, t, r);
+	}
+	if(alt){
+		if(!quotes)
+			return fmtstrcpy(f, s);
+		if(quotes & Qliteral)
+			return fmtstrcpy(f, "GOK");
+	}else if(quotes & Qliteral)
+		return fmtprint(f, "{%lud}\r\n%s", strlen(s), 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, '"');
+}
+
+int
+Xfmt(Fmt *f)
+{
+	char *s, buf[Pathlen], buf2[Pathlen];
+
+	s = va_arg(f->args, char*);
+	if(s == 0)
+		return fmtstrcpy(f, "NIL");
+	s = decmutf7(buf2, sizeof buf2, s);
+	cleanname(s);
+	return fmtstrcpy(f, encfs(buf, sizeof buf, s));
+}
+
+int
+Dfmt(Fmt *f)
+{
+	char buf[128], *fmt;
+	Tm *tm, t;
+	Tzone *tz;
+
+	tm = va_arg(f->args, Tm*);
+	if(tm == nil){
+		tz = tzload("local");
+		tm = tmtime(&t, time(0), tz);
+	}
+	if((f->flags & FmtSharp) == 0){
+		/* rfc822 style */
+		fmt = "WW, DD MMM YYYY hh:mm:ss Z";
+	}else
+		fmt = "DD-MMM-YYYY hh:mm:ss Z";
+	if(f->r == L'δ')
+		return fmtprint(f, "%τ", tmfmt(tm, fmt));
+	snprint(buf, sizeof(buf), "%τ", tmfmt(tm, fmt));
+	return fmtprint(f, "%Z", buf);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/quota.c
@@ -1,0 +1,73 @@
+#include "imap4d.h"
+
+static int
+openpipe(int *pip, char *cmd, char *av[])
+{
+	int pid, fd[2];
+
+	if(pipe(fd) != 0)
+		sysfatal("pipe: %r");
+	pid = fork();
+	switch(pid){
+	case -1:
+		return -1;
+	case 0:
+		close(1);
+		dup(fd[1], 1);
+		if(fd[1] != 1)
+			close(fd[1]);
+		if(fd[0] != 0)
+			close(fd[0]);
+		exec(cmd, av);
+		ilog("exec: %r");
+		_exits("b0rked");
+		return -1;
+	default:
+		*pip = fd[0];
+		close(fd[1]);
+		return pid;
+	}
+}
+
+static int
+closepipe(int pid, int fd)
+{
+	int nz, wpid;
+	Waitmsg *w;
+
+	close(fd);
+	while(w = wait()){
+		nz = !w->msg || !w->msg[0];
+		wpid = w->pid;
+		free(w);
+		if(wpid == pid)
+			return nz? 0: -1;
+	}
+	return -1;
+}
+
+static char dupath[Pathlen];
+static char *duav[] = { "du", "-s", dupath, 0};
+
+vlong
+getquota(void)
+{
+	char buf[Pathlen + 128], *f[3];
+	int fd, pid;
+
+	werrstr("");
+	memset(buf, 0, sizeof buf);
+	snprint(dupath, sizeof dupath, "%s", mboxdir);
+	pid = openpipe(&fd, "/bin/du", duav);
+	if(pid == -1)
+		return -1;
+	if(read(fd, buf, sizeof buf) < 4){
+		closepipe(pid, fd);
+		return -1;
+	}
+	if(closepipe(pid, fd) == -1)
+		return -1;
+	if(getfields(buf, f, 2, 1, "\t") != 2)
+		return -1;
+	return strtoull(f[0], 0, 0);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/search.c
@@ -1,0 +1,329 @@
+#include "imap4d.h"
+
+static int
+filesearch(Msg *m, char *file, char *pat)
+{
+	char buf[Bufsize + 1];
+	int n, nbuf, npat, fd, ok;
+
+	npat = strlen(pat);
+	if(npat >= Bufsize/2)
+		return 0;
+
+	fd = msgfile(m, file);
+	if(fd < 0)
+		return 0;
+	ok = 0;
+	nbuf = 0;
+	for(;;){
+		n = read(fd, &buf[nbuf], Bufsize - nbuf);
+		if(n <= 0)
+			break;
+		nbuf += n;
+		buf[nbuf] = '\0';
+		if(cistrstr(buf, pat) != nil){
+			ok = 1;
+			break;
+		}
+		if(nbuf > npat){
+			memmove(buf, &buf[nbuf - npat], npat);
+			nbuf = npat;
+		}
+	}
+	close(fd);
+	return ok;
+}
+
+static int
+headersearch(Msg *m, char *hdr, char *pat)
+{
+	char *s, *t;
+	int ok, n;
+	Slist hdrs;
+
+	n = m->head.size + 3;
+	s = emalloc(n);
+	hdrs.next = nil;
+	hdrs.s = hdr;
+	ok = 0;
+	if(selectfields(s, n, m->head.buf, &hdrs, 1) > 0){
+		t = strchr(s, ':');
+		if(t != nil && cistrstr(t + 1, pat) != nil)
+			ok = 1;
+	}
+	free(s);
+	return ok;
+}
+
+static int
+addrsearch(Maddr *a, char *s)
+{
+	char *ok, *addr;
+
+	for(; a != nil; a = a->next){
+		addr = maddrstr(a);
+		ok = cistrstr(addr, s);
+		free(addr);
+		if(ok != nil)
+			return 1;
+	}
+	return 0;
+}
+
+static int
+datecmp(char *date, Search *s)
+{
+	Tm tm;
+
+	date2tm(&tm, date);
+	if(tm.year < s->year)
+		return -1;
+	if(tm.year > s->year)
+		return 1;
+	if(tm.mon < s->mon)
+		return -1;
+	if(tm.mon > s->mon)
+		return 1;
+	if(tm.mday < s->mday)
+		return -1;
+	if(tm.mday > s->mday)
+		return 1;
+	return 0;
+}
+
+enum{
+	Simp	= 0,
+	Sinfo	= 1<<0,
+	Sbody	= 1<<2,
+};
+
+int
+searchld(Search *s)
+{
+	int r;
+
+	for(r = 0; (r & Sbody) == 0 && s; s = s->next)
+	switch(s->key){
+	case SKall:
+	case SKanswered:
+	case SKdeleted:
+	case SKdraft:
+	case SKflagged:
+	case SKkeyword:
+	case SKnew:
+	case SKold:
+	case SKrecent:
+	case SKseen:
+	case SKunanswered:
+	case SKundeleted:
+	case SKundraft:
+	case SKunflagged:
+	case SKunkeyword:
+	case SKunseen:
+	case SKuid:
+	case SKset:
+		break;
+	case SKlarger:
+	case SKsmaller:
+	case SKbcc:
+	case SKcc:
+	case SKfrom:
+	case SKto:
+	case SKsubject:
+	case SKbefore:
+	case SKon:
+	case SKsince:
+	case SKsentbefore:
+	case SKsenton:
+	case SKsentsince:
+		r = Sinfo;
+		break;
+	case SKheader:
+		if(cistrcmp(s->hdr, "message-id") == 0)
+			r = Sinfo;
+		else
+			r = Sbody;
+		break;
+	case SKbody:
+		break;		/* msgstruct doesn't do us any good */
+	case SKtext:
+	default:
+		r = Sbody;
+		break;
+	case SKnot:
+		r = searchld(s->left);
+		break;
+	case SKor:
+		r = searchld(s->left) | searchld(s->right);
+		break;
+	}
+	return 0;
+}
+
+/* important speed hack for apple mail */
+int
+msgidsearch(char *s, char *hdr)
+{
+	char c;
+	int l, r;
+
+	l = strlen(s);
+	c = 0;
+	if(s[0] == '<' && s[l-1] == '>'){
+		l -= 2;
+		s += 1;
+		c = s[l-1];
+	}
+	r = hdr && strstr(s, hdr) != nil;
+	if(c)
+		s[l-1] = c;
+	return r;
+}
+
+/*
+ * free to exit, parseerr, since called before starting any client reply
+ *
+ * the header and envelope searches should convert mime character set escapes.
+ */
+int
+searchmsg(Msg *m, Search *s, int ld)
+{
+	uint ok, id;
+	Msgset *ms;
+
+	if(m->expunged)
+		return 0;
+	if(ld & Sbody){
+		if(!msgstruct(m, 1))
+			return 0;
+	}else if (ld & Sinfo){
+		if(!msginfo(m))
+			return 0;
+	}
+	for(ok = 1; ok && s != nil; s = s->next){
+		switch(s->key){
+		default:
+			ok = 0;
+			break;
+		case SKnot:
+			ok = !searchmsg(m, s->left, ld);
+			break;
+		case SKor:
+			ok = searchmsg(m, s->left, ld) || searchmsg(m, s->right, ld);
+			break;
+		case SKall:
+			ok = 1;
+			break;
+		case SKanswered:
+			ok = (m->flags & Fanswered) == Fanswered;
+			break;
+		case SKdeleted:
+			ok = (m->flags & Fdeleted) == Fdeleted;
+			break;
+		case SKdraft:
+			ok = (m->flags & Fdraft) == Fdraft;
+			break;
+		case SKflagged:
+			ok = (m->flags & Fflagged) == Fflagged;
+			break;
+		case SKkeyword:
+			ok = (m->flags & s->num) == s->num;
+			break;
+		case SKnew:
+			ok = (m->flags & (Frecent|Fseen)) == Frecent;
+			break;
+		case SKold:
+			ok = (m->flags & Frecent) != Frecent;
+			break;
+		case SKrecent:
+			ok = (m->flags & Frecent) == Frecent;
+			break;
+		case SKseen:
+			ok = (m->flags & Fseen) == Fseen;
+			break;
+		case SKunanswered:
+			ok = (m->flags & Fanswered) != Fanswered;
+			break;
+		case SKundeleted:
+			ok = (m->flags & Fdeleted) != Fdeleted;
+			break;
+		case SKundraft:
+			ok = (m->flags & Fdraft) != Fdraft;
+			break;
+		case SKunflagged:
+			ok = (m->flags & Fflagged) != Fflagged;
+			break;
+		case SKunkeyword:
+			ok = (m->flags & s->num) != s->num;
+			break;
+		case SKunseen:
+			ok = (m->flags & Fseen) != Fseen;
+			break;
+		case SKlarger:
+			ok = msgsize(m) > s->num;
+			break;
+		case SKsmaller:
+			ok = msgsize(m) < s->num;
+			break;
+		case SKbcc:
+			ok = addrsearch(m->bcc, s->s);
+			break;
+		case SKcc:
+			ok = addrsearch(m->cc, s->s);
+			break;
+		case SKfrom:
+			ok = addrsearch(m->from, s->s);
+			break;
+		case SKto:
+			ok = addrsearch(m->to, s->s);
+			break;
+		case SKsubject:
+			ok = cistrstr(m->info[Isubject], s->s) != nil;
+			break;
+		case SKbefore:
+			ok = datecmp(m->info[Iunixdate], s) < 0;
+			break;
+		case SKon:
+			ok = datecmp(m->info[Iunixdate], s) == 0;
+			break;
+		case SKsince:
+			ok = datecmp(m->info[Iunixdate], s) > 0;
+			break;
+		case SKsentbefore:
+			ok = datecmp(m->info[Idate], s) < 0;
+			break;
+		case SKsenton:
+			ok = datecmp(m->info[Idate], s) == 0;
+			break;
+		case SKsentsince:
+			ok = datecmp(m->info[Idate], s) > 0;
+			break;
+		case SKuid:
+			id = m->uid;
+			goto set;
+		case SKset:
+			id = m->seq;
+		set:
+			for(ms = s->set; ms != nil; ms = ms->next)
+				if(id >= ms->from && id <= ms->to)
+					break;
+			ok = ms != nil;
+			break;
+		case SKheader:
+			if(cistrcmp(s->hdr, "message-id") == 0)
+				ok = msgidsearch(s->s, m->info[Imessageid]);
+			else
+				ok = headersearch(m, s->hdr, s->s);
+			break;
+		case SKbody:
+		case SKtext:
+			if(s->key == SKtext && cistrstr(m->head.buf, s->s)){
+				ok = 1;
+				break;
+			}
+			ok = filesearch(m, "body", s->s);
+			break;
+		}
+	}
+	return ok;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/store.c
@@ -1,0 +1,119 @@
+#include "imap4d.h"
+
+static Namedint	flagmap[] =
+{
+	{"\\Seen",	Fseen},
+	{"\\Answered",	Fanswered},
+	{"\\Flagged",	Fflagged},
+	{"\\Deleted",	Fdeleted},
+	{"\\Draft",	Fdraft},
+	{"\\Recent",	Frecent},
+	{nil,		0}
+};
+
+int
+storemsg(Box *box, Msg *m, int uids, void *vst)
+{
+	int f, flags;
+	Store *st;
+
+	if(m->expunged)
+		return uids;
+	st = vst;
+	flags = st->flags;
+
+	f = m->flags;
+	if(st->sign == '+')
+		f |= flags;
+	else if(st->sign == '-')
+		f &= ~flags;
+	else
+		f = flags;
+
+	/*
+	 * not allowed to change the recent flag
+	 */
+	f = (f & ~Frecent) | (m->flags & Frecent);
+	setflags(box, m, f);
+
+	if(st->op != Stflagssilent){
+		m->sendflags = 1;
+		box->sendflags = 1;
+	}
+
+	return 1;
+}
+
+/*
+ * update flags & global flag counts in box
+ */
+void
+setflags(Box *box, Msg *m, int f)
+{
+	if(f == m->flags)
+		return;
+	box->dirtyimp = 1;
+	if((f & Frecent) != (m->flags & Frecent)){
+		if(f & Frecent)
+			box->recent++;
+		else
+			box->recent--;
+	}
+	m->flags = f;
+}
+
+void
+sendflags(Box *box, int uids)
+{
+	Msg *m;
+
+	if(!box->sendflags)
+		return;
+
+	box->sendflags = 0;
+	for(m = box->msgs; m != nil; m = m->next){
+		if(!m->expunged && m->sendflags){
+			Bprint(&bout, "* %ud FETCH (", m->seq);
+			if(uids)
+				Bprint(&bout, "uid %ud ", m->uid);
+			Bprint(&bout, "FLAGS (");
+			writeflags(&bout, m, 1);
+			Bprint(&bout, "))\r\n");
+			m->sendflags = 0;
+		}
+	}
+}
+
+void
+writeflags(Biobuf *b, Msg *m, int recentok)
+{
+	char *sep;
+	int f;
+
+	sep = "";
+	for(f = 0; flagmap[f].name != nil; f++){
+		if((m->flags & flagmap[f].v)
+		&& (flagmap[f].v != Frecent || recentok)){
+			Bprint(b, "%s%s", sep, flagmap[f].name);
+			sep = " ";
+		}
+	}
+}
+
+int
+msgseen(Box *box, Msg *m)
+{
+	if(m->flags & Fseen)
+		return 0;
+	m->flags |= Fseen;
+	box->sendflags = 1;
+	m->sendflags = 1;
+	box->dirtyimp = 1;
+	return 1;
+}
+
+uint
+mapflag(char *name)
+{
+	return mapint(flagmap, name);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/imap4d/utils.c
@@ -1,0 +1,211 @@
+#include "imap4d.h"
+
+/*
+ * reverse string [s:e) in place
+ */
+void
+strrev(char *s, char *e)
+{
+	int c;
+
+	while(--e > s){
+		c = *s;
+		*s++ = *e;
+		*e = c;
+	}
+}
+
+int
+isdotdot(char *s)
+{
+	return s[0] == '.' && s[1] == '.' && (s[2] == '/' || s[2] == 0);
+}
+
+int
+issuffix(char *suf, char *s)
+{
+	int n;
+
+	n = strlen(s) - strlen(suf);
+	if(n < 0)
+		return 0;
+	return strcmp(s + n, suf) == 0;
+}
+
+int
+isprefix(char *pre, char *s)
+{
+	return strncmp(pre, s, strlen(pre)) == 0;
+}
+
+char*
+readfile(int fd)
+{
+	char *s;
+	long length;
+	Dir *d;
+
+	d = dirfstat(fd);
+	if(d == nil)
+		return nil;
+	length = d->length;
+	free(d);
+	s = binalloc(&parsebin, length + 1, 0);
+	if(s == nil || readn(fd, s, length) != length)
+		return nil;
+	s[length] = 0;
+	return s;
+}
+
+/*
+ * create the imap tmp file.
+ * it just happens that we don't need multiple temporary files.
+ */
+int
+imaptmp(void)
+{
+	char buf[ERRMAX], name[Pathlen];
+	int tries, fd;
+
+	snprint(name, sizeof name, "/mail/box/%s/mbox.tmp.imp", username);
+	for(tries = 0; tries < Locksecs*2; tries++){
+		fd = create(name, ORDWR|ORCLOSE|OCEXEC, DMEXCL|0600);
+		if(fd >= 0)
+			return fd;
+		errstr(buf, sizeof buf);
+		if(cistrstr(buf, "locked") == nil)
+			break;
+		sleep(500);
+	}
+	return -1;
+}
+
+/*
+ * open a file which might be locked.
+ * if it is, spin until available
+ */
+static char *etab[] = {
+	"not found",
+	"does not exist",
+	"file locked",		// hjfs
+	"file is locked",
+	"exclusive lock",
+	"already exists",
+};
+
+static int
+bad(int idx)
+{
+	char buf[ERRMAX];
+	int i;
+
+	rerrstr(buf, sizeof buf);
+	for(i = idx; i < nelem(etab); i++)
+		if(strstr(buf, etab[i]))
+			return 0;
+	return 1;
+}
+
+int
+openlocked(char *dir, char *file, int mode)
+{
+	int i, fd;
+
+	for(i = 0; i < 30; i++){
+		if((fd = cdopen(dir, file, mode)) >= 0 || bad(0))
+			return fd;
+		if((fd = cdcreate(dir, file, mode|OEXCL, DMEXCL|0600)) >= 0  || bad(2))
+			return fd;
+		sleep(1000);
+	}
+	werrstr("lock timeout");
+	return -1;
+}
+
+int
+fqid(int fd, Qid *qid)
+{
+	Dir *d;
+
+	d = dirfstat(fd);
+	if(d == nil)
+		return -1;
+	*qid = d->qid;
+	free(d);
+	return 0;
+}
+
+uint
+mapint(Namedint *map, char *name)
+{
+	int i;
+
+	for(i = 0; map[i].name != nil; i++)
+		if(cistrcmp(map[i].name, name) == 0)
+			break;
+	return map[i].v;
+}
+
+char*
+estrdup(char *s)
+{
+	char *t;
+
+	t = emalloc(strlen(s) + 1);
+	strcpy(t, s);
+	return t;
+}
+
+void*
+emalloc(ulong n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		bye("server out of memory");
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void*
+ezmalloc(ulong n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		bye("server out of memory");
+	setmalloctag(p, getcallerpc(&n));
+	memset(p, 0, n);
+	return p;
+}
+
+void*
+erealloc(void *p, ulong n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		bye("server out of memory");
+	setrealloctag(p, getcallerpc(&p));
+	return p;
+}
+
+void
+setname(char *fmt, ...)
+{
+	char buf[128], *p;
+	int fd;
+	va_list arg;
+
+	snprint(buf, sizeof buf, "/proc/%d/args", getpid());
+	if((fd = open(buf, OWRITE)) < 0)
+		return;
+
+	va_start(arg, fmt);
+	p = vseprint(buf, buf + sizeof buf, fmt, arg);
+	va_end(arg);
+
+	write(fd, buf, p - buf);
+	close(fd);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/marshal/marshal.c
@@ -1,0 +1,1832 @@
+/*
+ * marshal - gather mail message for transmission
+ */
+#include "common.h"
+#include <ctype.h>
+
+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	ainline;
+	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,
+	Hattach,
+	Hinclude,
+	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",
+[Hattach]	"attach:",
+[Hinclude]	"include:",
+};
+
+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;
+
+void	Bdrain(Biobuf*);
+void	attachment(Attach*, Biobuf*);
+void	body(Biobuf*, Biobuf*, int);
+int	doublequote(Fmt*);
+void*	emalloc(int);
+void*	erealloc(void*, int);
+char*	estrdup(char*);
+Addr*	expand(int, char**);
+Addr*	expandline(String**, Addr*);
+void	freeaddr(Addr*);
+void	freeaddr(Addr *);
+void	freeaddrs(Addr*);
+void	freealias(Alias*);
+void	freealiases(Alias*);
+Attach*	mkattach(char*, char*, int);
+char*	mkboundary(void);
+char*	hdrval(char*);
+char*	mksubject(char*);
+int	pgpfilter(int*, int, int);
+int	pgpopts(char*);
+int	printcc(Biobuf*, Addr*);
+int	printdate(Biobuf*);
+int	printfrom(Biobuf*);
+int	printinreplyto(Biobuf*, char*);
+int	printsubject(Biobuf*, char*);
+int	printto(Biobuf*, Addr*);
+Alias*	readaliases(void);
+int	readheaders(Biobuf*, int*, String**, Addr**, Addr**, Addr**, Attach**, int);
+void	readmimetypes(void);
+int	rfc2047fmt(Fmt*);
+int	sendmail(Addr*, Addr*, Addr*, int*, char*);
+char*	waitforsubprocs(void);
+
+int rflag, lbflag, xflag, holding, nflag, Fflag, eightflag, dflag;
+int pgpflag = 0;
+char *user;
+char *login;
+Alias *aliases;
+int rfc822syntaxerror;
+int attachfailed;
+char lastchar;
+char *replymsg;
+
+#define Rfc822fmt	"WW, DD MMM YYYY hh:mm:ss Z"
+enum
+{
+	Ok = 0,
+	Nomessage = 1,
+	Nobody = 2,
+	Error = -1,
+};
+
+#pragma varargck	type	"Z"	char*
+#pragma varargck	type	"U"	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);
+}
+
+static void
+bwritesfree(Biobuf *bp, String **str)
+{
+	if(Bwrite(bp, s_to_c(*str), s_len(*str)) != s_len(*str))
+		fatal("write error");
+	s_free(*str);
+	*str = nil;
+}
+
+void
+main(int argc, char **argv)
+{
+	int ccargc, bccargc, flags, fd, noinput, headersrv;
+	char *subject, *type, *boundary, *saveto;
+	char *ccargv[32], *bccargv[32];
+	Addr *to, *cc, *bcc;
+	Attach *first, **l, *a;
+	Biobuf in, out, *b;
+	String *hdrstring;
+	char file[Pathlen];
+
+	noinput = 0;
+	subject = nil;
+	first = nil;
+	l = &first;
+	type = nil;
+	hdrstring = nil;
+	saveto = nil;
+	ccargc = bccargc = 0;
+
+	tmfmtinstall();
+	quotefmtinstall();
+	fmtinstall('Z', doublequote);
+	fmtinstall('U', rfc2047fmt);
+
+	ARGBEGIN{
+	case 'a':
+		flags = 0;
+		goto aflag;
+	case 'A':
+		flags = 1;
+	aflag:
+		a = mkattach(EARGF(usage()), 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++] = EARGF(usage());
+		break;
+	case 'B':
+		if(bccargc >= nelem(bccargv)-1)
+			sysfatal("too many bcc's");
+		bccargv[bccargc++] = EARGF(usage());
+		break;
+	case 'd':
+		dflag = 1;		/* for sendmail */
+		break;
+	case 'F':
+		Fflag = 1;		/* file message */
+		break;
+	case 'S':
+		saveto = EARGF(usage());
+		break;
+	case 'n':			/* no standard input */
+		nflag = 1;
+		break;
+	case 'p':			/* pgp flag: encrypt, sign, or both */
+		if(pgpopts(EARGF(usage())) < 0)
+			sysfatal("bad pgp options");
+		break;
+	case 'r':
+		rflag = 1;		/* for sendmail */
+		break;
+	case 'R':
+		replymsg = EARGF(usage());
+		break;
+	case 's':
+		subject = EARGF(usage());
+		break;
+	case 't':
+		type = EARGF(usage());
+		break;
+	case 'x':
+		xflag = 1;		/* for sendmail */
+		break;
+	case '8':			/* read recipients from rfc822 header */
+		eightflag = 1;
+		break;
+	case '#':
+		lbflag = 1;		/* for sendmail */
+		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();
+
+	aliases = readaliases();
+	to = cc = bcc = nil;
+	if(argc > 0)
+		to = expand(argc, argv);
+	if(ccargc > 0)
+		cc = expand(ccargc, ccargv);
+	if(bccargc > 0)
+		bcc = expand(bccargc, bccargv);
+
+	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, eightflag? &cc: nil, eightflag? &bcc: nil, l, 1);
+		if(attachfailed){
+			Bdrain(&in);
+			fatal("attachment(s) failed, message not sent");
+		}
+		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;
+		}
+	}
+
+	if(Fflag)
+		saveto=argc>0?argv[0]:to->v;
+	fd = sendmail(to, cc, bcc, &pid, saveto);
+	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)
+		bwritesfree(&out, &hdrstring);
+
+	/* read user's standard headers */
+	mboxpathbuf(file, sizeof file, user, "headers");
+	if(b = Bopen(file, OREAD)){
+		if (readheaders(b, &flags, &hdrstring, nil, nil, nil, nil, 0) == Error)
+			fatal("reading");
+		Bterm(b);
+		bwritesfree(&out, &hdrstring);
+	}
+
+	/* add any headers we need */
+	if((flags & (1<<Hdate)) == 0)
+		if(printdate(&out) < 0)
+			fatal("writing");
+	if((flags & (1<<Hfrom)) == 0)
+		if(printfrom(&out) < 0)
+			fatal("writing");
+	if((flags & (1<<Hto)) == 0)
+		if(printto(&out, to) < 0)
+			fatal("writing");
+	if((flags & (1<<Hcc)) == 0)
+		if(printcc(&out, cc) < 0)
+			fatal("writing");
+	if((flags & (1<<Hsubject)) == 0 && subject != nil)
+		if(printsubject(&out, subject) < 0)
+			fatal("writing");
+	if(replymsg != nil)
+		if(printinreplyto(&out, replymsg) < 0)
+			fatal("writing");
+	Bprint(&out, "MIME-Version: 1.0\n");
+
+	if(pgpflag){
+		/* interpose pgp process between us and sendmail to handle body */
+		Bflush(&out);
+		Bterm(&out);
+		fd = pgpfilter(&pgppid, fd, pgpflag);
+		if(Binit(&out, fd, OWRITE) < 0)
+			fatal("can't Binit 1: %r");
+	}
+
+	/* if attachments, stick in multipart headers */
+	boundary = nil;
+	if(first != nil){
+		boundary = mkboundary();
+		Bprint(&out, "Content-Type: multipart/mixed;\n");
+		Bprint(&out, "\tboundary=\"%s\"\n\n", boundary);
+		Bprint(&out, "This is a multi-part message in MIME format.\n");
+		Bprint(&out, "--%s\n", boundary);
+		Bprint(&out, "Content-Disposition: inline\n");
+	}
+
+	if(!nflag){
+		if(!noinput && headersrv == Ok)
+			body(&in, &out, 1);
+	} else
+		Bprint(&out, "\n");
+	holdoff(holding);
+
+	Bflush(&out);
+	for(a = first; a != nil; a = a->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, Addr **ccp, Addr **bccp, Attach **att, int strict)
+{
+	int i, seen, hdrtype;
+	Addr *to, *cc, *bcc;
+	String *s, *sline;
+	char *p;
+
+	s = s_new();
+	to = cc = bcc = nil;
+	sline = 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) {
+			switch(hdrtype){
+			default:
+			Addhdr:
+				s_append(s, s_to_c(sline));
+				s_append(s, "\n");
+				break;
+			case Hto:
+				if(top)
+					to = expandline(&sline, to);
+				goto Addhdr;
+			case Hcc:
+				if(ccp)
+					cc = expandline(&sline, cc);
+				goto Addhdr;
+			case Hbcc:
+				if(bccp)
+					bcc = expandline(&sline, bcc);
+				break;
+			case Hsubject:
+				s_append(s, mksubject(s_to_c(sline)));
+				s_append(s, "\n");
+				break;
+			case Hattach:
+			case Hinclude:
+				if(att == nil)
+					break;
+				*att = mkattach(hdrval(s_to_c(sline)), nil, hdrtype == Hinclude);
+				if(*att == nil){
+					attachfailed = 1;
+					return Error;
+				}
+				att = &(*att)->next;
+				break;
+			}
+			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 users who 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<<i;
+				hdrtype = i;
+				break;
+			}
+		}
+		if(strict){
+			if(hdrtype == -1){
+				p[Blinelen(in)-1] = '\n';
+				Bseek(in, -Blinelen(in), 1);
+				break;
+			}
+		} else
+			hdrtype = 0;
+		p[Blinelen(in)-1] = '\n';
+	}
+
+	*sp = s;
+
+	if(to){
+		freeaddrs(*top);
+		*top = to;
+	}else
+		freeaddrs(to);
+
+	if(cc){
+		freeaddrs(*ccp);
+		*ccp = cc;
+	}else
+		freeaddrs(cc);
+
+	if(bcc){
+		freeaddrs(*bccp);
+		*bccp = bcc;
+	}else
+		freeaddrs(bcc);
+
+	if(seen == 0){
+		if(Blinelen(in) == 0)
+			return Nomessage;
+		else
+			return Ok;
+	}
+	if(p == nil)
+		return Nobody;
+	return Ok;
+}
+
+/* pass the body to sendmail, make sure body starts and ends with a newline */
+void
+body(Biobuf *in, Biobuf *out, int docontenttype)
+{
+	char *buf, *p;
+	int i, n, len;
+
+	n = 0;
+	len = 16*1024;
+	buf = emalloc(len);
+
+	/* first char must be newline */
+	i = Bgetc(in);
+	if(i > 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.
+ */
+static int
+enc64x18(char *out, int lim, uchar *in, int n)
+{
+	int m, mm, nn;
+
+	for(nn = 0; n > 0; n -= m, nn += mm){
+		m = 18 * 3;
+		if(m > n)
+			m = n;
+		nn++;	/* \n */
+		assert(nn < lim);
+		mm = enc64(out, lim - nn, in, m);
+		assert(mm > 0);
+		in += m;
+		out += mm;
+		*out++ = '\n';
+	}
+	return nn;
+}
+
+void
+body64(Biobuf *in, Biobuf *out)
+{
+	int m, n;
+	uchar buf[3*18*54];
+	char obuf[3*18*54*2];
+
+	Bprint(out, "\n");
+	for(;;){
+		n = Bread(in, buf, sizeof(buf));
+		if(n < 0)
+			fatal("input error");
+		if(n == 0)
+			break;
+		m = enc64x18(obuf, sizeof(obuf), buf, n);
+		if(Bwrite(out, obuf, m) < 0)
+			fatal("output error");
+	}
+	lastchar = '\n';
+}
+
+/* pass message to sendmail, make sure body starts with a newline */
+void
+copy(Biobuf *in, Biobuf *out)
+{
+	int n;
+	char buf[4*1024];
+
+	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->ainline)
+		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);
+}
+
+int
+printdate(Biobuf *b)
+{
+	Tm *tm;
+
+	tm = localtime(time(0));
+	return Bprint(b, "Date: %τ\n", tmfmt(tm, Rfc822fmt));
+}
+
+int
+printfrom(Biobuf *b)
+{
+	char *s;
+	int n;
+
+	if((n = strlen(user)) > 4 && user[n-1] == '>'){
+		if((s = strrchr(user, '<')) != nil && s != user && isspace(s[-1]))
+			return Bprint(b, "From: %.*U%s\n", (int)(s-user-1), user, s-1);
+	}
+
+	return Bprint(b, "From: %s\n", user);
+}
+
+int
+printaddr(Biobuf *b, char *s, Addr *a)
+{
+	int i;
+
+	if(a == nil)
+		return 0;
+	if(Bprint(b, "%s %s", 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
+printto(Biobuf *b, Addr *a)
+{
+	return printaddr(b, "To:", a);
+}
+
+int
+printcc(Biobuf *b, Addr *a)
+{
+	return printaddr(b, "Cc:", a);
+}
+
+int
+printsubject(Biobuf *b, char *subject)
+{
+	return Bprint(b, "Subject: %U\n", subject);
+}
+
+int
+printinreplyto(Biobuf *out, char *dir)
+{
+	int fd, n;
+	char buf[256];
+	String *s = s_copy(dir);
+
+	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);
+}
+
+int
+hassuffix(char *a, char *b)
+{
+	int na, nb;
+
+	na = strlen(a), nb = strlen(b);
+	if(na <= nb + 1 || a[na - nb - 1] != '.')
+		return 0;
+	return strcmp(a + (na - nb), b) == 0;
+}
+
+Attach*
+mkattach(char *file, char *type, int ainline)
+{
+	int n, pfd[2];
+	char ftype[64];
+	Attach *a;
+	Ctype *c;
+
+	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 = estrdup(file);
+	a->next = nil;
+	a->type = type;
+	a->ainline = ainline;
+	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 */
+	for(c = ctype; c->ext != nil; c++)
+		if(hassuffix(file, c->ext)){
+			a->type = c->type;
+			a->ctype = c;
+			return a;
+		}
+
+	/* try the mime types file */
+	if(mimetypes == nil)
+		readmimetypes();
+	for(c = mimetypes; c != nil && c->ext != nil; c++)
+		if(hassuffix(file, c->ext)){
+			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("/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)
+{
+	int i;
+	char buf[32];
+
+	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)
+{
+	int n;
+	char buf[8*1024];
+
+	while ((n = read(in, buf, sizeof buf)) > 0){
+		if(out1 != -1 && write(out1, buf, n) != n)
+			break;
+		if(out2 != -1 && write(out2, buf, n) != n)
+			break;
+	}
+}
+
+/* print the unix from line */
+int
+printunixfrom(int fd)
+{
+	Tm *tm;
+
+	tm = localtime(time(0));
+	return fprint(fd, "From %s %τ\n", user, tmfmt(tm, Rfc822fmt));
+}
+
+char *specialfile[] =
+{
+	"pipeto",
+	"pipefrom",
+	"L.mbox",
+	"forward",
+	"names"
+};
+
+/* return 1 if this is a special file */
+static int
+special(String *s)
+{
+	int i;
+	char *p;
+
+	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;
+}
+
+/* start up sendmail and return an fd to talk to it with */
+int
+sendmail(Addr *to, Addr *cc, Addr *bcc, int *pid, char *rcvr)
+{
+	int ac, fd, pfd[2];
+	char **v, *f, cmd[Pathlen];
+	Addr *a;
+	Biobuf *b;
+
+	ac = 0;
+	for(a = to; a != nil; a = a->next)
+		ac++;
+	for(a = cc; a != nil; a = a->next)
+		ac++;
+	for(a = bcc; a != nil; a = a->next)
+		ac++;
+	v = 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;
+	for(a = bcc; a != nil; a = a->next)
+		v[ac++] = a->v;
+	v[ac] = 0;
+
+	if(pipe(pfd) < 0)
+		fatal("%r");
+	switch(*pid = rfork(RFFDG|RFREND|RFPROC|RFENVG)){
+	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]);
+				/* BOTCH; "From " time gets changed */
+				f = foldername(nil, login, rcvr);
+				b = openfolder(f, time(0));
+				if(b != nil){
+					fd = Bfildes(b);
+					printunixfrom(fd);
+					tee(0, pfd[1], fd);
+					write(fd, "\n", 1);
+					closefolder(b);
+				}else{
+					fprint(2, "warning: open %s: %r", f);
+					tee(0, pfd[1], -1);
+				}
+				exits(0);
+			default:
+				close(pfd[1]);
+				dup(pfd[0], 0);
+				break;
+			}
+		}
+
+		if(replymsg != nil)
+			putenv("replymsg", replymsg);
+		mboxpathbuf(cmd, sizeof cmd, login, "pipefrom");
+		exec(cmd, v);
+		exec("/bin/myupassend", v);
+		exec("/bin/upas/send", v);
+		fatal("execing: %r");
+		break;
+	default:
+		free(v);
+		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)
+{
+	int ac;
+	int pfd[2];
+	char **av, **v;
+
+	v = av = emalloc(sizeof(char*)*8);
+	ac = 0;
+	v[ac++] = "pgp";
+	v[ac++] = "-fat";		/* operate as a filter, generate text */
+	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);
+
+		/* add newline to avoid confusing pgp output with 822 headers */
+		write(1, "\n", 1);
+		exec("/bin/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;
+}
+
+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)
+{
+	char file[Pathlen];
+	Addr *addr, **al;
+	Alias *a, **l, *first;
+	Sinstack *sp;
+	String *line, *token;
+	static int already;
+
+	first = nil;
+	line = s_new();
+	token = s_new();
+
+	/* open and get length */
+	mboxpathbuf(file, Pathlen, login, "names");
+	sp = s_allocinstack(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;
+		while(s_parse(line, s_restart(token)) != 0) {
+			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(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)
+{
+	Addr *first, *next, **l, *a;
+	Alias *al;
+
+	*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)
+{
+	int i;
+	Addr *first, **l;
+
+	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)
+{
+	int type, quoting;
+	char *op;
+
+	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)
+{
+	int tok, inangle, hadangle, nword;
+	char *p;
+	Addr *na, *nto, *ap;
+	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)
+{
+	char *p;
+	char type[256];
+	char *f[6];
+	Biobuf *b;
+	static int alloced, inuse;
+
+	if(mimetypes == 0){
+		alloced = 256;
+		mimetypes = emalloc(alloced*sizeof(Ctype));
+		mimetypes[0].ext = "";
+	}
+
+	b = Bopen("/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)
+{
+	int w, quotes;
+	char *s, *t;
+	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, '"');
+}
+
+int
+rfc2047fmt(Fmt *fmt)
+{
+	char *s, *p, *e;
+
+	s = va_arg(fmt->args, char*);
+	if(s == nil)
+		return fmtstrcpy(fmt, "");
+	e = s + ((fmt->flags & FmtPrec) ? fmt->prec : strlen(s));
+	for(p=s; *p && p != e; p++)
+		if((uchar)*p >= 0x80)
+			goto hard;
+	return fmtprint(fmt, "%.*s", (int)(e-s), s);
+
+hard:
+	fmtprint(fmt, "=?utf-8?q?");
+	for(p = s; *p && p != e; p++){
+		if(*p == ' ')
+			fmtrune(fmt, '_');
+		else if(*p == '_' || *p == '\t' || *p == '=' || *p == '?' ||
+		    (uchar)*p >= 0x80)
+			fmtprint(fmt, "=%.2uX", (uchar)*p);
+		else
+			fmtrune(fmt, (uchar)*p);
+	}
+	fmtprint(fmt, "?=");
+	return 0;
+}
+
+char*
+hdrval(char *p)
+{
+	char *e;
+
+	p = strchr(p, ':') + 1;
+	while(*p == ' ' || *p == '\t')
+		p++;
+	e = strchr(p, 0) - 1;
+	while(e >= p && (*e == ' ' || *e == '\t'))
+		*e-- = 0;
+	return p;
+}
+
+char*
+mksubject(char *line)
+{
+	char *p, *q;
+	static char buf[1024];
+
+	p = hdrval(line);
+	for(q = p; *q; q++)
+		if((uchar)*q >= 0x80)
+			goto hard;
+	return line;
+
+hard:
+	snprint(buf, sizeof buf, "Subject: %U", p);
+	return buf;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/marshal/mkfile
@@ -1,0 +1,11 @@
+</$objtype/mkfile
+
+TARG=marshal
+LIB=../common/libcommon.a$O
+OFILES=marshal.$O
+HFILES=../common/common.h
+
+</sys/src/cmd/mkone
+<../mkupas
+CFLAGS=$CFLAGS -I../common
+
--- /dev/null
+++ b/sys/src/cmd/upas/mkfile
@@ -1,0 +1,56 @@
+</$objtype/mkfile
+
+LIBS=common
+PROGS=\
+	Mail\
+	alias\
+	bayes\
+	binscripts\
+	dkim\
+	filterkit\
+	fs\
+	imap4d\
+	marshal\
+	ml\
+	ned\
+	pop3\
+	q\
+	scanmail\
+	send\
+	smtp\
+	spf\
+	unesc\
+	vf\
+
+#libs must be made first
+DIRS=$LIBS $PROGS
+
+default:V:
+	mk all
+
+all install installall clean nuke:V:
+	for (i in $DIRS) @{
+		cd $i
+		mk $target
+	}
+
+safeinstallall:V:
+	for (i in $LIBS) @{
+		cd $i
+		mk installall
+	}
+	for (i in $PROGS) @{
+		cd $i
+		mk safeinstallall
+	}
+
+test:V:
+	for (i in $LIBS) @{
+		cd $i
+		mk test
+	}
+	for (i in $PROGS) @{
+		cd $i
+		mk test
+	}
+
--- /dev/null
+++ b/sys/src/cmd/upas/mkupas
@@ -1,0 +1,5 @@
+BIN=/$objtype/bin/upas
+ABIN=/acme/bin/$objtype
+
+../common/libcommon.a$O:
+	cd ../common; mk
--- /dev/null
+++ b/sys/src/cmd/upas/ml/common.c
@@ -1,0 +1,205 @@
+#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 message 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){
+		sendnotification(addr, listname, rem);
+		fprint(fd, "!%s\n", addr);
+	}else{
+		fprint(fd, "%s\n", addr);
+		if(*addr != '#')
+			sendnotification(addr, listname, rem);
+	}
+	close(fd);
+}
+
+void
+remaddr(char *addr)
+{
+	Addr **l;
+	Addr *a;
+
+	for(l = &addrlist; *l; l = &(*l)->next){
+		a = *l;
+		if(strcmp(addr, a->addr) == 0){
+			(*l) = a->next;
+			free(a);
+			naddrlist--;
+			break;
+		}
+	}
+}
+
+int
+addaddr(char *addr)
+{
+	Addr **l;
+	Addr *a;
+
+	for(l = &addrlist; *l; l = &(*l)->next)
+		if(strcmp(addr, (*l)->addr) == 0)
+			return 0;
+	naddrlist++;
+	*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);
+}
+
+static void
+setsender(char *name)
+{
+	char *s;
+
+	s = smprint("%s-bounces", name);
+	putenv("upasname", s);
+	free(s);
+}
+
+/* start a mailer sending to all the receivers */
+int
+startmailer(char *name)
+{
+	char **av;
+	int pfd[2], ac;
+	Addr *a;
+
+	setsender(name);
+	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*)*(naddrlist+2));
+	if(av == nil)
+		sysfatal("starting mailer: %r");
+	ac = 0;
+	av[ac++] = name;
+	for(a = addrlist; 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;
+
+	setsender(listname);
+	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 been 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;
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/ml/dat.h
@@ -1,0 +1,25 @@
+
+#include "../smtp/smtp.h"
+#include "../smtp/rfc822.tab.h"
+
+typedef struct Addr Addr;
+struct Addr
+{
+	char *addr;
+	Addr *next;
+};
+
+String *from;
+String *sender;
+Field *firstfield;
+int naddrlist;
+Addr *addrlist;
+
+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);
--- /dev/null
+++ b/sys/src/cmd/upas/ml/mkfile
@@ -1,0 +1,26 @@
+</$objtype/mkfile
+
+TARG=\
+	ml\
+	mlowner\
+	mlmgr\
+
+LIB=../common/libcommon.a$O
+
+OFILES=common.$O
+
+HFILES=\
+	../common/common.h\
+	../common/sys.h\
+	dat.h\
+	../smtp/rfc822.tab.h\
+
+</sys/src/cmd/mkmany
+<../mkupas
+CFLAGS=$CFLAGS -I../common
+
+$O.ml: ../smtp/rfc822.tab.$O
+$O.mlowner: ../smtp/rfc822.tab.$O
+
+../smtp/rfc822.tab.h ../smtp/rfc822.tab.$O: ../smtp/rfc822.y
+	cd ../smtp && mk rfc822.tab.h rfc822.tab.$O
--- /dev/null
+++ b/sys/src/cmd/upas/ml/ml.c
@@ -1,0 +1,176 @@
+#include "common.h"
+#include "dat.h"
+
+Biobuf	in;
+Addr	*al;
+int	na;
+String	*from;
+String	*sender;
+
+char*
+trim(char *s)
+{
+	while(*s == ' ' || *s == '\t')
+		s++;
+	return s;
+}
+
+/* add the listname to the subject */
+void
+printsubject(int fd, Field *f, char *listname)
+{
+	char *s, *e, *ln;
+	Node *p;
+
+	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, trim(s));
+	else
+		fprint(fd, "Subject: %s\n", trim(s));
+	free(ln);
+	*e = '\n';
+}
+
+/* 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;
+		switch(f->node->c){
+		case SUBJECT:
+			subject = f;
+		case REPLY_TO:
+		case PRECEDENCE:
+			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 exists, cat the mail to the end of it */
+void
+appendtoarchive(char* listname, String *firstline, String *msg)
+{
+	char *f;
+	Biobuf *b;
+
+	f = foldername(nil, listname, "mbox");
+	if(access(f, 0) < 0)
+		return;
+	if((b = openfolder(f, time(0))) == nil)
+		return;
+	Bwrite(b, s_to_c(firstline), s_len(firstline));
+	Bwrite(b, s_to_c(msg), s_len(msg));
+	Bwrite(b, "\n", 1);
+	closefolder(b);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s address-list-file listname\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *listname, *alfile, *replytoname;
+	int fd, private;
+	String *msg, *firstline;
+	Waitmsg *w;
+
+	private = 0;
+	replytoname = nil;
+	ARGBEGIN{
+	default:
+		usage();
+	case 'p':
+		private = 1;
+		break;
+	case 'r':
+		replytoname = EARGF(usage());
+		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 ridiculous. 
+	 *   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");
+	if(addaddr(s_to_c(from)) != 0 && private)
+		sysfatal("not a list member");
+
+	/* 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 exists, cat the mail to the end of it */
+	appendtoarchive(listname, firstline, msg);
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/ml/mlmgr.c
@@ -1,0 +1,126 @@
+#include "common.h"
+#include "dat.h"
+
+enum {
+	Bounces,
+	Owner,
+	List,
+	Nboxes,
+};
+
+char *suffix[Nboxes] = {
+[Bounces]	"-bounces",
+[Owner]		"-owner",
+[List]		"",
+};
+
+int
+createpipeto(char *alfile, char *user, char *listname, char *dom, int which)
+{
+	char buf[Pathlen], rflag[64];
+	int fd;
+	Dir *d;
+
+	mboxpathbuf(buf, sizeof buf, user, "pipeto");
+
+	fprint(2, "creating new pipeto: %s\n", buf);
+	fd = create(buf, OWRITE, 0775);
+	if(fd < 0)
+		return -1;
+	d = dirfstat(fd);
+	if(d == nil){
+		fprint(fd, "Couldn't stat %s: %r\n", buf);
+		return -1;
+	}
+	d->mode |= 0775;
+	if(dirfwstat(fd, d) < 0)
+		fprint(fd, "Couldn't wstat %s: %r\n", buf);
+	free(d);
+
+	if(dom != nil)
+		snprint(rflag, sizeof rflag, "-r%s@%s ", listname, dom);
+	else
+		rflag[0] = 0;
+
+	fprint(fd, "#!/bin/rc\n");
+	switch(which){
+	case Owner:
+		fprint(fd, "/bin/upas/mlowner %s %s\n", alfile, listname);
+		break;
+	case List:
+		fprint(fd, "/bin/upas/ml %s%s %s\n", rflag, alfile, user);
+		break;
+	case Bounces:
+		fprint(fd, "exit ''\n");
+		break;
+	}
+	close(fd);
+
+	return 0;
+}
+
+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, *dom, *addr, alfile[Pathlen], buf[64], flag[127];
+	int i;
+
+
+	rfork(RFENVG|RFREND);
+
+	memset(flag, 0, sizeof flag);
+	ARGBEGIN{
+	case 'c':
+	case 'r':
+	case 'a':
+		flag[ARGC()] = 1;
+		break;
+	default:
+		usage();
+	}ARGEND;
+
+	if(flag['a'] + flag['r'] + flag['c'] > 1){
+		fprint(2, "%s: -a, -r, and -c are mutually exclusive\n", argv0);
+		exits("usage");
+	}
+
+	if(argc < 1)
+		usage();
+
+	listname = argv[0];
+	if((dom = strchr(listname, '@')) != nil)
+		*dom++ = 0;
+	mboxpathbuf(alfile, sizeof alfile, listname, "address-list");
+
+	if(flag['c']){
+		for(i = 0; i < Nboxes; i++){
+			snprint(buf, sizeof buf, "%s%s", listname, suffix[i]);
+			if(creatembox(buf, nil) < 0)
+				sysfatal("creating %s's mbox: %r", buf);
+			if(createpipeto(alfile, buf, listname, dom, i) < 0)
+				sysfatal("creating %s's pipeto: %r", buf);
+		}
+		writeaddr(alfile, "# mlmgr c flag", 0, listname);
+	} else if(flag['r']){
+		if(argc != 2)
+			usage();
+		addr = argv[1];
+		writeaddr(alfile, "# mlmgr r flag", 0, listname);
+		writeaddr(alfile, addr, 1, listname);
+	} else if(flag['a']){
+		if(argc != 2)
+			usage();
+		addr = argv[1];
+		writeaddr(alfile, "# mlmgr a flag", 0, listname);
+		writeaddr(alfile, addr, 0, listname);
+	}
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/ml/mlowner.c
@@ -1,0 +1,66 @@
+#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{
+	default:
+		usage();
+	}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 ridiculous */
+	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);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/ned/mkfile
@@ -1,0 +1,10 @@
+</$objtype/mkfile
+
+TARG=nedmail
+LIB=../common/libcommon.a$O
+OFILES=nedmail.$O
+HFILES=../common/common.h
+
+</sys/src/cmd/mkone
+<../mkupas
+CFLAGS=$CFLAGS -I../common
--- /dev/null
+++ b/sys/src/cmd/upas/ned/nedmail.c
@@ -1,0 +1,2823 @@
+#include "common.h"
+#include <ctype.h>
+#include <plumb.h>
+#include <regexp.h>
+
+typedef struct Cmd Cmd;
+typedef struct Ctype Ctype;
+typedef struct Dirstats Dirstats;
+typedef struct Message Message;
+typedef Message* (Mfn)(Cmd*,Message*);
+
+enum{
+	/* nflags */
+	Nmissing	= 1<<0,
+	Nnoflags	= 1<<1,
+
+	Narg	= 32,
+};
+
+struct Message {
+	Message	*next;
+	Message	*prev;
+	Message	*cmd;
+	Message	*child;
+	Message	*parent;
+	char	*path;
+	int	id;
+	int	len;
+	int	fileno;	/* number of directory */
+	char	*info;
+	char	*from;
+	char	*to;
+	char	*cc;
+	char	*replyto;
+	char	*date;
+	char	*subject;
+	char	*type;
+	char	*disposition;
+	char	*filename;
+	uchar	flags;
+	uchar	nflags;
+};
+#pragma varargck	type	"D"	Message*
+
+enum{
+	Display	= 1<<0,
+	Rechk	= 1<<1,	/* always use file to check content type */
+};
+
+struct Ctype {
+	char	*type;
+	char 	*ext;
+	uchar	flag;
+	char	*plumbdest;
+	Ctype	*next;
+};
+
+/* first element is the default return value */
+Ctype ctype[] = {
+	{ "application/octet-stream", 	"bin", 	Rechk, 	0,	0,	},
+	{ "text/plain",			"txt",	Display,	0	},
+	{ "text/html",			"htm",	Display,	0	},
+	{ "text/html",			"html",	Display,	0	},
+	{ "text/tab-separated-values",	"tsv",	Display,	0	},
+	{ "text/richtext",			"rtx",	Display,	0	},
+	{ "text/rtf",			"rtf",	Display,	0	},
+	{ "text",				"txt",	Display,	0	},
+	{ "message/rfc822",		"msg",	0,	0	},
+	{ "image/bmp",			"bmp",	0,	"image"	},
+	{ "image/jpg",			"jpg",	0,	"image"	},
+	{ "image/jpeg",			"jpg",	0,	"image"	},
+	{ "image/gif",			"gif",	0,	"image"	},
+	{ "image/png",			"png",	0,	"image"	},
+	{ "image/x-png",			"png",	0,	"image"	},
+	{ "image/tiff",			"tif",	0,	"image"	},
+	{ "application/pdf",		"pdf",	0,	"postscript"	},
+	{ "application/postscript",		"ps",	0,	"postscript"	},
+	{ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",		"docx",	0,	"docx"	},
+	{ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",		"xlsx",	0,	"xlsx"	},
+	{ "application/",			0,	0,	0	},
+	{ "image/",			0,	0,	0	},
+	{ "multipart/",			"mul",	0,	0	},
+
+};
+
+struct Dirstats {
+	int	new;
+	int	del;
+	int	old;
+	int	unread;
+};
+
+Mfn	acmd;
+Mfn	bangcmd;
+Mfn	bcmd;
+Mfn	dcmd;
+Mfn	eqcmd;
+Mfn	Fcmd;
+Mfn	fcmd;
+Mfn	fqcmd;
+Mfn	Hcmd;
+Mfn	hcmd;
+Mfn	helpcmd;
+Mfn	icmd;
+Mfn	Kcmd;
+Mfn	kcmd;
+Mfn	mbcmd;
+Mfn	mcmd;
+Mfn	Pcmd;
+Mfn	pcmd;
+Mfn	pipecmd;
+Mfn	qcmd;
+Mfn	quotecmd;
+Mfn	rcmd;
+Mfn	rpipecmd;
+Mfn	scmd;
+Mfn	tcmd;
+Mfn	ucmd;
+Mfn	wcmd;
+Mfn	xcmd;
+Mfn	ycmd;
+
+struct {
+	char	*cmd;
+	int	args;
+	int	addr;
+	Mfn	*f;
+	char	*help;
+} cmdtab[] = {
+	{ "a",	1, 1,	acmd,	"a\t"		"reply to sender and recipients" },
+	{ "A",	1, 0,	acmd,	"A\t"		"reply to sender and recipients with copy" },
+	{ "b",	0, 0,	bcmd,	"b\t"		"print the next 10 headers" },
+	{ "d",	0, 1,	dcmd,	"d\t"		"mark for deletion" },
+	{ "F",	1, 1,	Fcmd,	"f\t"		"set message flags [+-][aDdfrSs]" },
+	{ "f",	0, 1,	fcmd,	"f\t"		"file message by from address" },
+	{ "fq",	0, 1,	fqcmd,	"fq\t"		"print mailbox f appends" },
+	{ "H",	0, 0,	Hcmd,	"H\t"		"print message's MIME structure" },
+	{ "h",	0, 0,	hcmd,	"h\t"		"print message summary (,h for all)" },
+	{ "help", 0, 0,	helpcmd, "help\t"		"print this info" },
+	{ "i",	0, 0,	icmd,	"i\t"		"incorporate new mail" },
+	{ "k",	1, 1,	kcmd,	"k [flags]\t"	"mark mail" },
+	{ "K",	1, 1,	Kcmd,	"K [flags]\t"	"unmark mail" },
+	{ "m",	1, 1,	mcmd,	"m addr\t"	"forward mail" },
+	{ "M",	1, 0,	mcmd,	"M addr\t"	"forward mail with message" },
+	{ "mb",	1, 0,	mbcmd,	"mb mbox\t"	"switch mailboxes" },
+	{ "p",	1, 0,	pcmd,	"p\t"		"print the processed message" },
+	{ "P",	0, 0,	Pcmd,	"P\t"		"print the raw message" },
+	{ "\"",	0, 0,	quotecmd, "\"\t"		"print a quoted version of msg" },
+	{ "\"\"",	0, 0,	quotecmd, "\"\"\t"		"format and quote message" },
+	{ "q",	0, 0,	qcmd,	"q\t"		"exit and remove all deleted mail" },
+	{ "r",	1, 1,	rcmd,	"r [addr]\t"	"reply to sender plus any addrs specified" },
+	{ "rf",	1, 0,	rcmd,	"rf [addr]\t"	"file message and reply" },
+	{ "R",	1, 0,	rcmd,	"R [addr]\t"	"reply including copy of message" },
+	{ "Rf",	1, 0,	rcmd,	"Rf [addr]\t"	"file message and reply with copy" },
+	{ "s",	1, 1,	scmd,	"s file\t"		"append raw message to file" },
+	{ "t",	1, 0,	tcmd,	"t\t"		"text formatter" },
+	{ "u",	0, 0,	ucmd,	"u\t"		"remove deletion mark" },
+	{ "w",	1, 1,	wcmd,	"w file\t"		"store message contents as file" },
+	{ "x",	0, 0,	xcmd,	"x\t"		"exit without flushing deleted messages" },
+	{ "y",	0, 0,	ycmd,	"y\t"		"synchronize with mail box" },
+	{ "=",	1, 0,	eqcmd,	"=\t"		"print current message number" },
+	{ "|",	1, 1,	pipecmd, "|cmd\t"		"pipe message body to a command" },
+	{ "||",	1, 1,	rpipecmd, "||cmd\t"	"pipe raw message to a command" },
+	{ "!",	1, 0,	bangcmd, "!cmd\t"		"run a command" },
+};
+
+struct Cmd {
+	Message	*msgs;
+	Mfn	*f;
+	int	an;
+	char	*av[Narg];
+	char	cmdline[2*1024];
+	int	delete;
+};
+
+int		dir2message(Message*, int, Dirstats*);
+int		mdir2message(Message*);
+char*		extendp(char*, char*);
+char*		parsecmd(char*, Cmd*, Message*, Message*);
+void		system(char*, char**, int);
+int		switchmb(char *, int);
+void		closemb(void);
+Message*	dosingleton(Message*, char*);
+char*		rooted(char*);
+int		plumb(Message*, Ctype*);
+void		exitfs(char*);
+Message*	flushdeleted(Message*);
+
+int	didopen;
+int	doflush;
+int	interrupted;
+int	longestfrom = 12;
+int	longestto = 12;
+int	hcmdfmt;
+Qid	mbqid;
+int	mbvers;
+char	mbname[Pathlen];
+char	mbpath[Pathlen];
+int	natural;
+Biobuf	out;
+int	reverse;
+char	root[Pathlen];
+int	rootlen;
+int	startedfs;
+Message	top;
+char	*user;
+char	homewd[Pathlen];
+char	wd[Pathlen];
+char	textfmt[Pathlen];
+
+char*
+idfmt(char *p, char *e, Message *m)
+{
+	char buf[32];
+	int sz, l;
+
+	for(; (sz = e - p) > 0; ){
+		l = snprint(buf, sizeof buf, "%d", m->id);
+		if(l + 1 > sz)
+			return "*GOK*";
+		e -= l;
+		memcpy(e, buf, l);
+		if((m = m->parent) == &top)
+			break;
+		e--;
+		*e = '.';
+	}
+	return e;
+}
+
+int
+eprint(char *fmt, ...)
+{
+	int n;
+	va_list args;
+
+	Bflush(&out);
+
+	va_start(args, fmt);
+	n = vfprint(2, fmt, args);
+	va_end(args);
+	return n;
+}
+
+void
+dissappeared(void)
+{
+	char buf[ERRMAX];
+
+	rerrstr(buf, sizeof buf);
+	if(strstr(buf, "hungup channel")){
+		eprint("\n!she's dead, jim\n");
+		exits(buf);
+	}
+	eprint("!message dissappeared\n");
+}
+
+int
+Dfmt(Fmt *f)
+{
+	char *e, buf[128];
+	Message *m;
+
+	m = va_arg(f->args, Message*);
+	if(m == nil)
+		return fmtstrcpy(f, "*GOK*");
+	if(m == &top)
+		return 0;
+	e = buf + sizeof buf - 1;
+	*e = 0;
+	return fmtstrcpy(f, idfmt(buf, e, m));
+}
+
+char*
+readline(char *prompt, char *line, int len)
+{
+	char *p, *e, *q;
+	int n, dump;
+
+	e = line + len;
+retry:
+	dump = 0;
+	interrupted = 0;
+	eprint("%s", prompt);
+	for(p = line;; p += n){
+		if(p == e){
+			dump = 1;
+			p = line;
+		}
+		n = read(0, p, e - p);
+		if(n < 0){
+			if(interrupted)
+				goto retry;
+			return nil;
+		}
+		if(n == 0)
+			return nil;
+		if(q = memchr(p, '\n', n)){
+			if(dump){
+				eprint("!line too long\n");
+				goto retry;
+			}
+			p = q;
+			break;
+		}
+	}
+	*p = 0;
+	return line;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-nrt] [-f mailfile] [-s mailfile]\n", argv0);
+	fprint(2, "       %s -c dir\n", argv0);
+	exits("usage");
+}
+
+void
+catchnote(void*, char *note)
+{
+	if(strstr(note, "interrupt") != nil){
+		interrupted = 1;
+		noted(NCONT);
+	}
+	noted(NDFLT);
+}
+
+char*
+plural(int n)
+{
+	if (n == 1)
+		return "";
+	return "s";	
+}
+
+void
+main(int argc, char **argv)
+{
+	char cmdline[2*1024], prompt[64], *err, *av[4], *mb;
+	int n, cflag, singleton;
+	Cmd cmd;
+	Ctype *cp;
+	Message *cur, *m, *x;
+
+	Binit(&out, 1, OWRITE);
+
+	mb = nil;
+	singleton = 0;
+	reverse = 1;
+	cflag = 0;
+	ARGBEGIN {
+	case 'c':
+		cflag = 1;
+		break;
+	case 's':
+		singleton = 1;
+	case 'f':
+		mb = EARGF(usage());
+		break;
+	case 'r':
+		reverse = 0;
+		break;
+	case 'n':
+		natural = 1;
+		reverse = 0;
+		break;
+	case 't':
+		hcmdfmt = 1;
+		break;
+	default:
+		usage();
+		break;
+	} ARGEND;
+
+	fmtinstall('D', Dfmt);
+	quotefmtinstall();
+	doquote = needsrcquote;
+	getwd(homewd, sizeof homewd);
+	user = getlog();
+	if(user == nil || *user == 0)
+		sysfatal("can't read user name");
+
+	if(cflag){
+		if(argc > 0)
+			n = creatembox(user, argv[0]);
+		else
+			n = creatembox(user, nil);
+		exits(n? 0: "fail");
+	}
+
+	if(argc)
+		usage();
+
+	if(access("/mail/fs/ctl", 0) < 0){
+		startedfs = 1;
+		av[0] = "fs";
+		av[1] = "-p";
+		av[2] = 0;
+		system("/bin/upas/fs", av, -1);
+	}
+
+	switchmb(mb, singleton);
+	top.path = strdup(root);
+	for(cp = ctype; cp < ctype + nelem(ctype) - 1; cp++)
+		cp->next = cp + 1;
+
+	if(singleton){
+		cur = dosingleton(&top, mb);
+		if(cur == nil){
+			eprint("no message\n");
+			exitfs(0);
+		}
+		pcmd(nil, cur);
+	} else {
+		cur = &top;
+		if(icmd(nil, cur) == nil)
+			sysfatal("can't read %s", top.path);
+	}
+
+	notify(catchnote);
+	for(;;){
+		snprint(prompt, sizeof prompt, "%D: ", cur);
+
+		/*
+		 * leave space at the end of cmd line in case parsecmd needs to
+		 * add a space after a '|' or '!'
+		 */
+		if(readline(prompt, cmdline, sizeof cmdline - 1) == nil)
+			break;
+		err = parsecmd(cmdline, &cmd, top.child, cur);
+		if(err != nil){
+			eprint("!%s\n", err);
+			continue;
+		}
+		if(singleton && (cmd.f == icmd || cmd.f == ycmd)){
+			eprint("!illegal command\n");
+			continue;
+		}
+		interrupted = 0;
+		if(cmd.msgs == nil || cmd.msgs == &top){
+			if(x = cmd.f(&cmd, &top))
+				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){
+						eprint("!address\n");
+						cur = x;
+						break;
+					} else
+						x = x->next;
+				}
+			}
+			x = cmd.f(&cmd, x);
+			if(x != nil)
+				cur = x;
+			if(interrupted)
+				break;
+			if(singleton && (cmd.delete || cmd.f == dcmd))
+				qcmd(nil, nil);
+		}
+		if(doflush)
+			cur = flushdeleted(cur);
+	}
+	qcmd(nil, nil);
+}
+
+char*
+file2string(char *dir, char *file)
+{
+	int fd, n;
+	char *s, *p, *e;
+
+	p = s = malloc(512);
+	e = p + 511;
+
+	fd = open(extendp(dir, file), OREAD);
+	while((n = read(fd, p, e - p)) > 0){
+		p += n;
+		if(p == e){
+			s = realloc(s, (n = p - s) + 512 + 1);
+			if(s == nil)
+				sysfatal("malloc: %r");
+			p = s + n;
+			e = p + 512;
+		}
+	}
+	close(fd);
+	*p = 0;
+	return s;
+}
+
+#define Fields 		18			/* terrible hack; worth 10% */
+#define Minfields	17
+
+void
+updateinfo(Message *m)
+{
+	char *s, *f[Fields + 1];
+	int i, n, sticky;
+
+	s = file2string(m->path, "info");
+	if(s == nil)
+		return;
+	if((n = getfields(s, f, nelem(f), 0, "\n")) < Minfields){
+		for(i = 0; i < n; i++)
+			fprint(2, "info: %s\n", f[i]);
+		sysfatal("info file invalid %s %D: %d fields", m->path, m, n);
+	}
+	if((m->nflags & Nnoflags) == 0){
+		sticky = m->flags & Fdeleted;
+		m->flags = buftoflags(f[17]) | sticky;
+	}
+	m->nflags &= ~Nmissing;
+	free(s);
+}
+
+Message*
+file2message(Message *parent, char *name)
+{
+	char *path, *f[Fields + 1];
+	int i, n;
+	Message *m;
+
+	m = mallocz(sizeof *m, 1);
+	if(m == nil)
+		return nil;
+	m->path = path = strdup(extendp(parent->path, name));
+	m->fileno = atoi(name);
+	m->info = file2string(path, "info");
+	m->parent = parent;
+	n = getfields(m->info, f, nelem(f), 0, "\n");
+	if(n < Minfields){
+		for(i = 0; i < n; i++)
+			fprint(2, "info: [%s]\n", f[i]);
+		sysfatal("info file invalid %s %D: %d fields", path, m, n);
+	}
+	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 = strtoul(f[16], 0, 0);
+	if(n > 17)
+		m->flags = buftoflags(f[17]);
+	else
+		m->nflags |= Nnoflags;
+
+	if(m->type)
+	if(strstr(m->type, "multipart") != nil || strcmp(m->type, "message/rfc822") == 0)
+		mdir2message(m);
+	return m;
+}
+
+void
+freemessage(Message *m)
+{
+	Message *nm, *next;
+
+	for(nm = m->child; nm != nil; nm = next){
+		next = nm->next;
+		freemessage(nm);
+	}
+	free(m->path);
+	free(m->info);
+	free(m);
+}
+
+/*
+ * read a directory into a list of messages.  at the top level, there may be
+ * large gaps in message numbers.  so we need to read the whole directory.
+ * and pick out the messages we're interested in.  within a message, subparts
+ * are contiguous and if we don't read the header/body/rawbody, we can avoid forcing
+ * upas/fs to read the whole message.
+ */
+int
+mdir2message(Message *parent)
+{
+	char buf[Pathlen];
+	int i, highest, newmsgs;
+	Dir *d;
+	Message *first, *last, *m;
+
+	/* 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;
+	for(i = highest + 1;; i++){
+		snprint(buf, sizeof buf, "%s/%d", parent->path, i);
+		if((d = dirstat(buf)) == nil)
+			break;
+		if((d->qid.type & QTDIR) == 0){
+			free(d);
+			continue;
+		}
+		free(d);
+		snprint(buf, sizeof buf, "%d", i);
+		m = file2message(parent, buf);
+		if(m == nil)
+			break;
+		m->id = m->fileno;
+		newmsgs++;
+		if(first == nil)
+			first = m;
+		else
+			last->next = m;
+		m->prev = last;
+		last = m;
+	}
+	parent->child = first;
+	return newmsgs;
+}
+
+/*
+ * 99.9% of the time, we don't need to sort this list.
+ * however, sometimes email is added to a mailbox
+ * out of order.  or, sape copies it back in from the
+ * dump.  in this case, we've got to sort.
+ *
+ * BOTCH.  we're not observing any sort of stable
+ * order.  if an old message comes in while upas/fs
+ * is running, it will appear out of order.  restarting
+ * upas/fs will reorder things.
+ */
+int
+dcmp(Dir *a, Dir *b)
+{
+	return atoi(a->name) - atoi(b->name);
+}
+
+void
+itsallsapesfault(Dir *d, int n)
+{
+	int c, i, r, t;
+
+	/* evade qsort suck */
+	r = -1;
+	for(i = 0; i < n; i++){
+		t = atol(d[i].name);
+		if(t > r){
+			c = d[i].name[0];
+			if(c >= '0' && c <= 9)
+				break;
+		}
+		r = t;
+	}
+	if(i != n)
+		qsort(d, n, sizeof *d, (int (*)(void*, void*))dcmp);
+}
+
+int
+dir2message(Message *parent, int reverse, Dirstats *s)
+{
+	int i, c, n, fd;
+	Dir *d;
+	Message *first, *last, *m, **ll;
+
+	memset(s, 0, sizeof *s);
+	fd = open(parent->path, OREAD);
+	if(fd < 0)
+		return -1;
+	first = parent->child;
+	last = nil;
+	if(first)
+		for(last = first; last->next; last = last->next)
+			;
+	n = dirreadall(fd, &d);
+	itsallsapesfault(d, n);
+	if(reverse)
+		ll = &last;
+	else
+		ll = &parent->child;
+	for(i = 0; *ll || i < n; ){
+		if(i < n && (d[i].qid.type & QTDIR) == 0){
+			i++;
+			continue;
+		}
+		c = -1;
+		if(i >= n)
+			c = 1;
+		else if(*ll)
+			c = atoi(d[i].name) - (*ll)->fileno;
+		if(c < 0){
+			m = file2message(parent, d[i].name);
+			if(m == nil)
+				break;
+			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;
+			}
+			*ll = m;
+			s->new++;
+			s->unread += (m->flags & Fseen) == 0;
+			i++;
+		}else if(c > 0){
+			(*ll)->nflags |= Nmissing;
+			s->del++;
+		}else{
+			updateinfo(*ll);
+			s->old++;
+			i++;
+		}
+
+		if(reverse)
+			ll = &(*ll)->prev;
+		else
+			ll = &(*ll)->next;
+	}
+	free(d);
+	close(fd);
+	parent->child = first;
+
+	/* renumber and file longest from */
+	i = 1;
+	longestfrom = 12;
+	longestto = 12;
+	for(m = first; m != nil; m = m->next){
+		m->id = natural ? m->fileno : i++;
+		n = strlen(m->from);
+		if(n > longestfrom)
+			longestfrom = n;
+		n = strlen(m->to);
+		if(n > longestto)
+			longestto = n;
+	}
+	return 0;
+}
+
+/*
+ *   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, m->path) == 0)
+				return m;
+		if(m == nil)
+			return nil;
+	}
+	return m;
+}
+
+/*
+ *   walk the path name an element
+ */
+char*
+extendp(char *dir, char *name)
+{
+	static char buf[Pathlen];
+
+	if(strcmp(dir, ".") == 0)
+		snprint(buf, sizeof buf, "%s", name);
+	else
+		snprint(buf, sizeof buf, "%s/%s", dir, name);
+	return buf;
+}
+
+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], *f[6], *dtime;
+	int n;
+	long now, then;
+	Tm tm;
+
+	*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);
+}
+
+int
+matchtype(char *s, Ctype *t)
+{
+	return strncmp(t->type, s, strlen(t->type)) == 0;
+}
+
+Ctype*
+findctype(Message *m)
+{
+	char *p, ftype[256];
+	int n, pfd[2];
+	Ctype *a, *cp;
+
+	for(cp = ctype; cp; cp = cp->next)
+		if(matchtype(m->type, cp))
+			if((cp->flag & Rechk) == 0)
+				return cp;
+			else
+				break;
+
+	if(pipe(pfd) < 0)
+		return ctype;
+	*ftype = 0;
+	switch(fork()){
+	case -1:
+		break;
+	case 0:
+		close(pfd[1]);
+		close(0);
+		dup(pfd[0], 0);
+		close(1);
+		dup(pfd[0], 1);
+		execl("/bin/file", "file", "-m", extendp(m->path, "body"), nil);
+		exits(0);
+	default:
+		close(pfd[0]);
+		n = read(pfd[1], ftype, sizeof ftype - 1);
+		while(n > 0 && isspace(ftype[n - 1]))
+			n--;
+		ftype[n] = 0;
+		close(pfd[1]);
+		waitpid();
+		break;
+	}
+	for(cp = ctype; cp; cp = cp->next)
+		if(matchtype(ftype, cp))
+			return cp;
+	if(*ftype == 0 || (p = strchr(ftype, '/')) == nil)
+		return ctype;
+	*p++ = 0;
+
+	a = mallocz(sizeof *a, 1);
+	a->type = strdup(ftype);
+	a->ext = strdup(p);
+	a->flag = 0;
+	a->plumbdest = strdup(ftype);
+	for(cp = ctype; cp->next; cp = cp->next)
+		;
+	cp->next = a;
+	a->next = nil;
+	return a;
+}
+
+/*
+ * traditional
+ */
+void
+hds(char *buf, Message *m)
+{
+	buf[0] = m->child? 'H': ' ';
+	buf[1] = m->flags & Fdeleted ? 'd' : ' ';
+	buf[2] = m->flags & Fstored? 's': ' ';
+	buf[3] = m->flags & Fseen? ' ': '*';
+	if(m->flags & Fanswered)
+		buf[3] = 'a';
+	if(m->flags & Fflagged)
+		buf[3] = '\'';
+	buf[4] = 0;
+}
+
+void
+pheader0(char *buf, int len, Message *m)
+{
+	char *f, *p, *q, frombuf[40], timebuf[32], h[5];
+	int max;
+
+	hds(h, m);
+	if(hcmdfmt == 0){
+		f = m->from;
+		max = longestfrom;
+	}else{
+		snprint(frombuf, sizeof frombuf-5, "%s", m->to);
+		p = strchr(frombuf, ' ');
+		if(p != nil)
+			snprint(p, 5, " ...");
+		f = frombuf;
+		max = longestto;
+		if(max > sizeof frombuf)
+			max = sizeof frombuf;
+	}
+
+	if(*f == 0)
+		snprint(buf, len, "%3D    %s %6d  %s",
+			m, 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, "%3D %s %6d  %11.11s %-*.*s %s",
+			m, h, m->len, timebuf, max, max, f, p);
+		free(q);
+	} else {
+		cracktime(m->date, timebuf, sizeof timebuf);
+		snprint(buf, len, "%3D %s %6d  %11.11s %s",
+			m, h, m->len, timebuf, f);
+	}
+}
+
+void
+pheader(char *buf, int len, int indent, Message *m)
+{
+	char *p, *e, typeid[80];
+
+	e = buf + len;
+	snprint(typeid, sizeof typeid, "%D    %s", m, 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);
+}
+
+char sstring[256];
+
+/*
+ * 	cmd := range cmd ' ' arg-list ;
+ * 	range := address
+ * 		| address ',' address
+ * 		| 'g' search ;
+ * 	address := msgno
+ * 		| search ;
+ * 	msgno := number
+ * 		| number '/' msgno ;
+ * 	search := '/' string '/'
+ * 		| '%' string '%'
+ *		| '#' (field '#')? re '#'
+ *
+ */
+static char*
+qstrchr(char *s, int c)
+{
+	for(;; s++){
+		if(*s == '\\')
+			s++;
+		else if(*s == c)
+			return s;
+		if(*s == 0)
+			return nil;
+	}
+}
+
+Reprog*
+parsesearch(char **pp, char *buf, int bufl)
+{
+	char *p, *np, *e;
+	int c, n;
+
+	buf[0] = 0;
+	p = *pp;
+	c = *p++;
+	if(c == '#')
+		snprint(buf, bufl, "from");
+	np = qstrchr(p, c);
+	if(c == '#' && np)
+	if(e = qstrchr(np + 1, c)){
+		snprint(buf, bufl, "%.*s", utfnlen(p, np - p), p);
+		p = np + 1;
+		np = e;
+	}
+	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);
+}
+
+enum{
+	Comma = 1,
+};
+
+/*
+ *   search a message for a regular expression match
+ */
+int
+fsearch(Message *m, Reprog *prog, char *field)
+{
+	char buf[4096 + 1];
+	int i, fd, rv;
+	uvlong o;
+
+	rv = 0;
+	fd = open(extendp(m->path, field), OREAD);
+	/*
+	 *  march through raw message 4096 bytes at a time
+	 *  with a 128 byte overlap to chain the re search.
+	 */
+	for(o = 0;; o += i - 128){
+		i = pread(fd, buf, sizeof buf - 1, o);
+		if(i <= 0)
+			break;
+		buf[i] = 0;
+		if(regexec(prog, buf, nil, 0)){
+			rv = 1;
+			break;
+		}
+		if(i < sizeof buf - 1)
+			break;
+	}
+	close(fd);
+	return rv;
+}
+
+int
+rsearch(Message *m, Reprog *prog, char*)
+{
+	return fsearch(m, prog, "raw");
+}
+
+int
+hsearch(Message *m, Reprog *prog, char*)
+{
+	char buf[256];
+
+	pheader0(buf, sizeof buf, m);
+	return regexec(prog, buf, nil, 0);
+}
+
+/*
+ * ack: returns int (*)(Message*, Reprog*, char*)
+ */
+int (*
+chartosearch(int c))(Message*, Reprog*, char*)
+{
+	switch(c){
+	case '%':
+		return rsearch;
+	case '/':
+	case '?':
+		return hsearch;
+	case '#':
+		return fsearch;
+	}
+	return 0;
+}
+
+char*
+parseaddr(char **pp, Message *first, Message *cur, Message *unspec, Message **mp, int f)
+{
+	char *p, buf[256];
+	int n, c, sign;
+	Message *m, *m0;
+	Reprog *prog;
+	int (*fn)(Message*, Reprog*, char*);
+
+	*mp = nil;
+	p = *pp;
+
+	sign = 0;
+	if(*p == '+'){
+		sign = 1;
+		p++;
+		*pp = p;
+	} else if(*p == '-'){
+		sign = -1;
+		p++;
+		*pp = p;
+	}
+
+	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 = &top;
+			break;
+		}
+	number:
+		m0 = m = nil;
+		switch(sign){
+		case 0:
+			for(m = first; m != nil; m0 = m, m = m->next)
+				if(m->id == n)
+					break;
+			break;
+		case -1:
+			if(cur != &top)
+				for(m = cur; m0 = m, m != nil && n > 0; n--)
+					m = m->prev;
+			break;
+		case 1:
+			if(cur == &top){
+				n--;
+				cur = first;
+			}
+			for(m = cur; m != nil && n > 0; m0 = m, n--)
+				m = m->next;
+			break;
+		}
+		if(m == nil && f&Comma)
+			m = m0;
+		if(m == nil)
+			return "address";
+		*mp = m;
+		break;
+	case '?':
+		/* legacy behavior.  no longer needed */
+		sign = -1;
+	case '%':
+	case '/':
+	case '#':
+		c = *p;
+		fn= chartosearch(c);
+		prog = parsesearch(pp, buf, sizeof buf);
+		if(prog == nil)
+			return "badly formed regular expression";
+		if(sign == -1){
+			for(m = cur == &top ? nil : cur->prev; m; m = m->prev)
+				if(fn(m, prog, buf))
+					break;
+		}else{
+			for(m = cur == &top ? first : cur->next; m; m = m->next)
+				if(fn(m, prog, buf))
+					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((m = (*mp)->child) == nil)
+			return "no sub parts";
+		return parseaddr(pp, m, m, m, mp, 0);
+	}
+	c = **pp;
+	if(c == '+' || c == '-' || c == '/' || c == '%' || c == '#')
+		return parseaddr(pp, first, *mp, *mp, mp, 0);
+
+	return nil;
+}
+
+char*
+parsecmd(char *p, Cmd *cmd, Message *first, Message *cur)
+{
+	char buf[256], *err;
+	int i, c, r;
+	Reprog *prog;
+	Message *m, *s, *e, **l, *last;
+	int (*f)(Message*, Reprog*, char*);
+	static char errbuf[ERRMAX];
+
+	cmd->delete = 0;
+	l = &cmd->msgs;
+	*l = nil;
+
+	while(*p == ' ' || *p == '\t')
+		p++;
+
+	/* null command is a special case (advance and print) */
+	if(*p == 0){
+		if(cur == &top)
+			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 == 'k'){
+			for(m = first; m != nil; m = m->next)
+			if(m->flags & Fflagged){
+				*l = m;
+				l = &m->cmd;
+				*l = nil;
+			}
+			p++;
+		}else if(*p != '/' && *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;
+			f = chartosearch(c);
+			prog = parsesearch(&p, buf, sizeof buf);
+			if(prog == nil)
+				return "badly formed regular expression";
+			for(m = first; m != nil; m = m->next){
+				if(f(m, prog, buf)){
+					*l = m;
+					l = &m->cmd;
+					*l = nil;
+				}
+			}
+			free(prog);
+		}
+	}else{
+		/* parse an address */
+		s = e = nil;
+		err = parseaddr(&p, first, cur, cur, &s, 0);
+		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, Comma);
+			if(err != nil)
+				return err;
+			/* select all messages in the range */
+			r = 0;
+			if(s != nil && e != nil && s->id > e->id)
+				r = 1;
+			while(s != nil){
+				*l = s;
+				l = &s->cmd;
+				*l = nil;
+				if(s == e)
+					break;
+				if(r)
+					s = s->prev;
+				else
+					s = s->next;
+			}
+			if(s == nil)
+				return "null address range";
+		} else {
+			/* single address */
+			if(s != &top){
+				*l = s;
+				s->cmd = nil;
+			}
+		}
+	}
+
+	while(*p == ' ' || *p == '\t')
+		p++;
+	/* hack to allow all messages to start with 'd' */
+	if(*p == 'd' && p[1]){
+		cmd->delete = 1;
+		p++;
+	}
+	while(*p == ' ' || *p == '\t')
+		p++;
+	if(*p == 0)
+		p = "p";
+	for(i = nelem(cmdtab) - 1; i >= 0; i--)
+		if(strncmp(p, cmdtab[i].cmd, strlen(cmdtab[i].cmd)) == 0)
+			goto found;
+	return "illegal command";
+found:
+	p += strlen(cmdtab[i].cmd);
+	snprint(cmd->cmdline, sizeof cmd->cmdline, "%s", p);
+	cmd->av[0] = cmdtab[i].cmd;
+	cmd->an = 1 + tokenize(p, cmd->av + 1, nelem(cmd->av) - 2);
+	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;
+
+	if(cmdtab[i].addr && (cmd->msgs == nil || cmd->msgs == &top)){
+		snprint(errbuf, sizeof errbuf, "%s requires an address", cmdtab[i].cmd);
+		return errbuf;
+ 	}
+	return nil;
+}
+
+Message*
+aichcmd(Message *m, int indent)
+{
+	char hdr[256];
+
+	pheader(hdr, sizeof hdr, indent, m);
+	Bprint(&out, "%s\n", hdr);
+	for(m = m->child; m != nil; m = m->next)
+		aichcmd(m, indent + 1);
+	return m;
+}
+
+Message*
+Hcmd(Cmd*, Message *m)
+{
+	if(m == &top)
+		return nil;
+	return aichcmd(m, 0);
+}
+
+Message*
+hcmd(Cmd*, Message *m)
+{
+	char hdr[256];
+
+	if(m == &top)
+		return nil;
+	pheader0(hdr, sizeof hdr, m);
+	Bprint(&out, "%s\n", hdr);
+	return m;
+}
+
+Message*
+bcmd(Cmd*, Message *m)
+{
+	int i;
+	Message *om;
+
+	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 m != nil? m: om;
+}
+
+Message*
+ncmd(Cmd*, Message *m)
+{
+	if(m == &top)
+		return m->child;
+	return m->next;
+}
+
+int
+writepart(char *m, char *part, char *s)
+{
+	char *e;
+	int fd, n;
+
+	fd = open(extendp(m, part), OWRITE);
+	if(fd < 0){
+		dissappeared();
+		return -1;
+	}
+	for(e = s + strlen(s); e - s > 0; s += n){
+		if((n = write(fd, s, e - s)) <= 0){
+			eprint("!writepart:%s: %r\n", part);
+			break;
+		}
+		if(interrupted)
+			break;
+	}
+	close(fd);
+	return s == e? 0: -1;
+}
+
+Message	*xpipecmd(Cmd*, Message*, char*);
+
+Message*
+printfmt(Message *m, char *part, char *cmd)
+{
+	Cmd c;
+
+	c.an = 2;
+	snprint(c.cmdline, sizeof c.cmdline, "%s", cmd);
+	Bflush(&out);
+	return xpipecmd(&c, m, part);
+}
+
+int
+printpart0(Message *m, char *part)
+{
+	char buf[4096];
+	int n, fd, tot;
+
+	fd = open(extendp(m->path, part), OREAD);
+	if(fd < 0){
+		dissappeared();
+		return 0;
+	}
+	tot = 0;
+	while((n = read(fd, buf, sizeof buf)) > 0){
+		if(interrupted)
+			break;
+		if(Bwrite(&out, buf, n) <= 0)
+			break;
+		tot += n;
+	}
+	close(fd);
+	return tot;
+}
+
+int
+printpart(Message *m, char *part, char *cmd)
+{
+	if(cmd == nil || cmd[0] == 0)
+		return printpart0(m, part);
+	printfmt(m, part, cmd);
+	return 1;
+}
+
+int
+printhtml(Message *m)
+{
+	Cmd c;
+
+	memset(&c, 0, sizeof c);
+	c.an = 3;
+	snprint(c.cmdline, sizeof c.cmdline, "/bin/htmlfmt -l60 -cutf8");
+	eprint("!/bin/htmlfmt\n");
+	pipecmd(&c, m);
+	return 0;
+}
+
+Message*
+Pcmd(Cmd*, Message *m)
+{
+	if(m == &top)
+		return &top;
+	if(m->parent == &top)
+		printpart(m, "unixheader", nil);
+	printpart(m, "raw", nil);
+	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;
+}
+
+void
+setflags(Message *m, char *f)
+{
+	uchar f0;
+
+	f0 = m->flags;
+	txflags(f, &m->flags);
+	if(f0 != m->flags)
+		if((m->nflags & Nnoflags) == 0)
+			writepart(m->path, "flags", f);
+}
+
+Message*
+Fcmd(Cmd *c, Message *m)
+{
+	int i;
+
+	for(i = 1; i < c->an; i++)
+		setflags(m, c->av[i]);
+	return m;
+}
+
+void
+seen(Message *m)
+{
+	setflags(m, "s");
+}
+
+/*
+ * sleeze
+ */
+int
+magicpart(Message *m, char *s, char *part)
+{
+	char buf[4096];
+	int n, fd, c;
+
+	fd = open(extendp(s, part), OREAD);
+	if(fd < 0){
+		if(strcmp(part, "id") == 0)
+			Bprint(&out, "%D ", m);
+		else if(strcmp(part, "fpath") == 0)
+			Bprint(&out, "%s ", rooted(m->path));
+		else
+			Bprint(&out, "%s ", part);
+		return 0;
+	}
+
+	c = 0;
+	while((n = read(fd, buf, sizeof buf)) > 0){
+		c = -1;
+		if(interrupted)
+			break;
+		if(Bwrite(&out, buf, n) <= 0)
+			break;
+		c = buf[n - 1];
+	}
+	close(fd);
+	if(!interrupted && n != -1 && c != -1)
+	if(strstr(part, "body") != nil || strcmp(part, "rawunix") == 0)
+		seen(m);
+	return c;
+}
+
+Message*
+pcmd0(Cmd *c, Message *m, int mayplumb, char *tfmt)
+{
+	char *s, buf[128];
+	int i, ch;
+	Ctype *cp;
+	Message *nm;
+
+	if(m == &top)
+		return &top;
+	if(c && c->an >= 2){
+		ch = 0;
+		for(i = 1; i < c->an; i++)
+			ch = magicpart(m, m->path, c->av[i]);
+		if(ch != '\n')
+			Bprint(&out, "\n");
+		return m;
+	}
+	if(m->parent == &top){
+		seen(m);
+		printpart(m, "unixheader", nil);
+	}
+	if(printpart(m, "header", nil) > 0)
+		Bprint(&out, "\n");
+	cp = findctype(m);
+	if(cp->flag & Display){
+		if(strcmp(m->type, "text/html") == 0)
+			printhtml(m);
+		else
+			printpart(m, "body", tfmt);
+	}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->flag & Display)
+					break;
+			}
+		if(nm != nil)
+			pcmd0(nil, nm, mayplumb, tfmt);
+		else
+			hcmd(nil, m);
+	}else if(strncmp(m->type, "multipart/", 10) == 0){
+		nm = m->child;
+		if(nm != nil){
+			/* always print first part */
+			pcmd0(nil, nm, mayplumb, tfmt);
+
+			for(nm = nm->next; nm != nil; nm = nm->next){
+				s = rooted(nm->path);
+				cp = findctype(nm);
+				pheader(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, cp->ext);
+					else
+						Bprint(&out, "\n--- %s %s/body\n\n",
+							buf, s);
+					pcmd0(nil, nm, 0, tfmt);
+				} else {
+					if(cp->ext != nil)
+						Bprint(&out, "\n!--- %s %s/body.%s\n",
+							buf, s, cp->ext);
+					else
+						Bprint(&out, "\n!--- %s %s/body\n",
+							buf, s);
+				}
+			}
+		} else {
+			hcmd(nil, m);
+		}
+	}else if(strcmp(m->type, "message/rfc822") == 0)
+		pcmd(nil, m->child);
+	else if(!mayplumb){
+	}else if(plumb(m, cp) >= 0){
+		Bprint(&out, "\n!--- using plumber to type %s", cp->type);
+		if(strcmp(cp->type, m->type) != 0)
+			Bprint(&out, " (was %s)", m->type);
+		Bprint(&out, "\n");
+	}else
+		Bprint(&out, "\n!--- cannot display %s\n", cp->type);
+
+	return m;
+}
+
+Message*
+pcmd(Cmd *c, Message *m)
+{
+	return pcmd0(c, m, 1, textfmt);
+}
+
+Message*
+tcmd(Cmd *c, Message *m)
+{
+	switch(c->an){
+	case 1:
+		if(textfmt[0] != 0)
+			textfmt[0] = 0;
+		else
+			snprint(textfmt, sizeof textfmt, "%s", "upas/tfmt");
+		break;
+	default:
+		snprint(textfmt, sizeof textfmt, "%s", c->cmdline);
+		break;
+	}
+	eprint("!textfmt %s\n", textfmt);
+	return m;
+}
+
+void
+printpartindented(char *s, char *part, char *indent)
+{
+	char *p;
+	Biobuf *b;
+
+	b = Bopen(extendp(s, part), OREAD);
+	if(b == nil){
+		dissappeared();
+		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);
+}
+
+void
+printpartindent2(char *s, char *part, char *indent)
+{
+	Cmd c;
+
+	memset(&c, 0, sizeof c);
+	snprint(c.cmdline, sizeof c.cmdline, "fmt -q '> ' %s | sed 's/^/%s/g' ",
+		rooted(extendp(s, part)), indent);
+	Bflush(&out);
+	bangcmd(&c, nil);
+}
+
+Message*
+quotecmd0(Cmd *c, Message *m, void (*p)(char*, char*, char*))
+{
+	Ctype *cp;
+	Message *nm;
+
+	if(m == &top)
+		return &top;
+	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->flag & Display)
+		p(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->flag & Display)
+					break;
+			}
+		if(nm != nil)
+			quotecmd(c, nm);
+	}else if(strncmp(m->type, "multipart/", 10) == 0){
+		nm = m->child;
+		if(nm != nil){
+			cp = findctype(nm);
+			if(cp->flag & Display || strncmp(m->type, "multipart/", 10) == 0)
+				quotecmd(c, nm);
+		}
+	}
+	return m;
+}
+
+Message*
+quotecmd(Cmd *c, Message *m)
+{
+	void (*p)(char*, char*, char*);
+
+	p = printpartindented;
+	if(strstr(c->av[0], "\"\"") != nil)
+		p = printpartindent2;
+	return quotecmd0(c, m, p);
+}
+
+
+/* really delete messages */
+Message*
+flushdeleted(Message *cur)
+{
+	char buf[1024], *p, *e, *msg;
+	int i, deld, n, fd;
+	Message *m, **l;
+
+	doflush = 0;
+	deld = 0;
+
+	fd = open("/mail/fs/ctl", ORDWR);
+	if(fd < 0){
+		eprint("!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->nflags & Nmissing) == 0)
+		if((m->flags & Fdeleted) == 0){
+			l = &(*l)->next;
+			continue;
+		}
+
+		/* don't return a pointer to a deleted message */
+		if(m == cur)
+			cur = m->next;
+		deld++;
+		if(m->flags & Fdeleted){
+			msg = strrchr(m->path, '/');
+			if(msg == nil)
+				msg = m->path;
+			else
+				msg++;
+			if(e - p < 10){
+				write(fd, 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)
+		write(fd, buf, p - buf);
+
+	close(fd);
+
+	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 &top;
+	}
+	return cur;
+}
+
+Message*
+mbcmd(Cmd *c, Message*)
+{
+	char *mb, oldmb[Pathlen];
+	Message *m, **l;
+
+	switch(c->an){
+	case 1:
+		mb = "mbox";
+		break;
+	case 2:
+		mb = c->av[1];
+		break;
+	default:
+		eprint("!usage: mbcmd [mbox]\n");
+		return nil;	
+	}
+
+	/* flushdeleted(nil); ? */
+	for(l = &top.child; *l; ){
+		m = *l;
+		*l = m->next;
+		freemessage(m);
+	}
+	top.child = nil;
+
+	strcpy(oldmb, mbpath);
+	if(switchmb(mb, 0) < 0){
+		eprint("!no mb\n");
+		if(switchmb(oldmb, 0) < 0){
+			eprint("!mb disappeared\n");
+			exits("fail");
+		}
+	}
+	icmd(nil, nil);
+	interrupted = 1;	/* no looping */
+	return &top;
+}
+
+Message*
+qcmd(Cmd*, Message*)
+{
+	flushdeleted(nil);
+	if(didopen)
+		closemb();
+	Bflush(&out);
+	exitfs(0);
+	return nil;
+}
+
+Message*
+ycmd(Cmd *c, Message *m)
+{
+	doflush = 1;
+	return icmd(c, m);
+}
+
+Message*
+xcmd(Cmd*, Message*)
+{
+	exitfs(0);
+	return nil;
+}
+
+Message*
+eqcmd(Cmd*, Message *m)
+{
+	Bprint(&out, "%D\n", m);
+	return m;
+}
+
+Message*
+dcmd(Cmd*, Message *m)
+{
+	while(m->parent != &top)
+		m = m->parent;
+	m->flags |= Fdeleted;
+	return m;
+}
+
+Message*
+ucmd(Cmd*, Message *m)
+{
+	if(m == &top)
+		return nil;
+	while(m->parent != &top)
+		m = m->parent;
+	m->flags &= ~Fdeleted;
+	return m;
+}
+
+int
+skipscan(void)
+{
+	int r;
+	Dir *d;
+	static int lastvers = -1;
+
+	d = dirstat(top.path);
+	r = d && d->qid.path == mbqid.path && d->qid.vers == mbqid.vers;
+	r = r && mbvers == lastvers;
+	if(d != nil){
+		mbqid = d->qid;
+		lastvers = mbvers;
+	}
+	free(d);
+	return r;
+}
+
+Message*
+icmd(Cmd *c, Message *m)
+{
+	char buf[128], *p, *e;
+	Dirstats s;
+
+	if(skipscan())
+		return m;
+	if(dir2message(&top, reverse, &s) < 0)
+		return nil;
+	p = buf;
+	e = buf + sizeof buf;
+	if(s.new > 0 && c == nil){
+		p = seprint(p, e, "%d message%s", s.new, plural(s.new));
+		if(s.unread > 0)
+			p = seprint(p, e, ", %d unread", s.unread);
+	}
+	else if(s.new > 0)
+		Bprint(&out, "%d new message%s", s.new, plural(s.new));
+	if(s.new && s.del)
+		p = seprint(p, e, "; ");
+	if(s.del > 0)
+		p = seprint(p, e, "%d deleted message%s", s.del, plural(s.del));
+	if(s.new + s.del)
+		p = seprint(p, e, "\n");
+	if(p > buf){
+		Bflush(&out);
+		eprint("%s", buf);
+	}
+	return m;
+}
+
+Message*
+kcmd0(Cmd *c, Message *m)
+{
+	char *f, *s;
+	int sticky;
+
+	if(c->an > 2){
+		eprint("!usage k [flags]\n");
+		return nil;
+	}
+	if(c->f == kcmd)
+		f = "f";
+	else
+		f = "-f";
+	if(c->an == 2)
+		f = c->av[1];
+	setflags(m, f);
+	if(c->an == 2 && (m->nflags & Nnoflags) == 0){
+		sticky = m->flags & Fdeleted;
+		s = file2string(m->path, "flags");
+		m->flags = buftoflags(s) | sticky;
+		free(s);
+	}
+	return m;
+}
+
+Message*
+kcmd(Cmd *c, Message *m)
+{
+	return kcmd0(c, m);
+}
+
+Message*
+Kcmd(Cmd *c, Message *m)
+{
+	return kcmd0(c, m);
+}
+
+Message*
+helpcmd(Cmd*, Message *m)
+{
+	int i;
+
+	Bprint(&out, "Commands are of the form [<range>] <command> [args]\n");
+	Bprint(&out, "<range> := <addr> | <addr>','<addr>| 'g'<search>\n");
+	Bprint(&out, "<addr> := '.' | '$' | '^' | <number> | <search> | <addr>'+'<addr> | <addr>'-'<addr>\n");
+	Bprint(&out, "<search> := 'k' | '/'<re>'/' | '?'<re>'?' | '%%'<re>'%%' | '#' <field> '#' <re> '#' \n");
+	Bprint(&out, "<command> :=\n");
+	for(i = 0; i < nelem(cmdtab); i++)
+		Bprint(&out, "%s\n", cmdtab[i].help);
+	return m;
+}
+
+/* ed thinks this is a good idea */
+void
+marshal(char **path, char **argv0)
+{
+	char *s;
+
+	s = getenv("marshal");
+	if(s == nil || *s == 0)
+		s = "/bin/upas/marshal";
+	*path = s;
+	*argv0 = strrchr(s, '/') + 1;
+	if(*argv0 == (char*)1)
+		*argv0 = s;
+}
+
+int
+tomailer(char **av)
+{
+	int pid, i;
+	char *p, *a;
+	Waitmsg *w;
+
+	switch(pid = fork()){
+	case -1:
+		eprint("can't fork: %r\n");
+		return -1;
+	case 0:
+		marshal(&p, &a);
+		Bprint(&out, "!%s", p);
+		for(i = 1; av[i]; i++)
+			Bprint(&out, " %q", av[i]);
+		Bprint(&out, "\n");
+		Bflush(&out);
+		av[0] = a;
+		chdir(wd);
+		exec(p, av);
+		eprint("couldn't exec %s\n", p);
+		exits(0);
+	default:
+		w = wait();
+		if(w == nil){
+			if(interrupted)
+				postnote(PNPROC, pid, "die");
+			waitpid();
+			return -1;
+		}
+		if(w->msg[0]){
+			eprint("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, intok, inquote;
+
+	if(max <= 0)
+		return 0;
+	intok = inquote = 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;
+		}
+}
+
+static char *rec[] = {"Re: ", "AW:", };
+static char *fwc[] = {"Fwd: ", };
+
+char*
+addrecolon(char **tab, int n, char *s)
+{
+	char *prefix;
+	int i;
+
+	prefix = "";
+	for(i = 0; i < n; i++)
+		if(cistrncmp(s, tab[i], strlen(tab[i]) - 1) == 0)
+			break;
+	if(i == n)
+		prefix = tab[0];
+	return smprint("%s%s", prefix, s);
+}
+
+Message*
+rcmd(Cmd *c, Message *m)
+{
+	char *from, *path, *subject, *rpath, *addr, *av[128];
+	int i, ai;
+	Message *nm;
+
+	ai = 1;
+	av[ai++] = "-8";
+	addr = path = subject = nil;
+	for(nm = m; nm != &top; nm = nm->parent)
+ 		if(*nm->replyto != 0){
+			addr = nm->replyto;
+			break;
+		}
+	if(addr == nil){
+		eprint("!no reply address\n");
+		return nil;
+	}
+
+	if(nm == &top){
+		print("!noone to reply to\n");
+		return nil;
+	}
+
+	for(nm = m; nm != &top; nm = nm->parent)
+		if(*nm->subject){
+			av[ai++] = "-s";
+			subject = addrecolon(rec, nelem(rec), nm->subject);
+			av[ai++] = subject;
+			break;
+		}
+
+	av[ai++] = "-R";
+	av[ai++] = rpath = strdup(rooted(m->path));
+
+	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 = strdup(rooted(extendp(m->path, "raw")));
+		av[ai++] = path;
+	}
+
+	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
+		av[ai++] = c->av[i];
+	ai += tokenize822(from = strdup(addr), &av[ai], nelem(av) - ai);
+	av[ai] = 0;
+	if(tomailer(av) == -1)
+		m = nil;
+	else
+		m->flags |= Fanswered;
+	free(path);
+	free(rpath);
+	free(subject);
+	free(from);
+	return m;
+}
+
+Message*
+mcmd(Cmd *c, Message *m)
+{
+	char *subject, *av[128];
+	int i, ai;
+
+	if(c->an < 2){
+		eprint("!usage: M list-of addresses\n");
+		return nil;
+	}
+
+	ai = 1;
+	subject = nil;
+	if(m->subject){
+		av[ai++] = "-s";
+		subject = addrecolon(fwc, nelem(fwc), m->subject);
+		av[ai++] = subject;
+	}
+
+	av[ai++] = "-t";
+	if(m->parent == &top)
+		av[ai++] = "message/rfc822";
+	else
+		av[ai++] = "mime";
+
+	av[ai++] = "-A";
+	av[ai++] = rooted(extendp(m->path, "raw"));
+	if(strchr(c->av[0], 'M') == nil)
+		av[ai++] = "-n";
+	else
+		av[ai++] = "-8";
+	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
+		av[ai++] = c->av[i];
+	av[ai] = 0;
+
+	if(tomailer(av) == -1)
+		m = nil;
+	else
+		m->flags |= Fanswered;
+	free(subject);
+	return m;
+}
+
+Message*
+acmd(Cmd *c, Message *m)
+{
+	char *av[128], *rpath, *subject, *from, *to, *cc;
+	int i, ai;
+
+	if(m->from == nil || m->to == nil || m->cc == nil){
+		eprint("!bad message\n");
+		return nil;
+	}
+
+	ai = 1;
+	av[ai++] = "-8";
+	av[ai++] = "-R";
+	av[ai++] = rpath = strdup(rooted(m->path));
+
+	subject = nil;
+	if(m->subject && *m->subject){
+		av[ai++] = "-s";
+		subject = addrecolon(rec, nelem(rec), m->subject);
+		av[ai++] = subject;
+	}
+
+	if(strchr(c->av[0], 'A') != nil){
+		av[ai++] = "-t";
+		av[ai++] = "message/rfc822";
+		av[ai++] = "-A";
+		av[ai++] = rooted(extendp(m->path, "raw"));
+	}
+
+	for(i = 1; i < c->an && ai < nelem(av)-1; i++)
+		av[ai++] = c->av[i];
+	ai += tokenize822(from = strdup(m->from), &av[ai], nelem(av) - ai);
+	ai += tokenize822(to = strdup(m->to), &av[ai], nelem(av) - ai);
+	ai += tokenize822(cc = strdup(m->cc), &av[ai], nelem(av) - ai);
+	av[ai] = 0;
+	if(tomailer(av) == -1)
+		m = nil;
+	else
+		m->flags |= Fanswered;
+	free(from);
+	free(to);
+	free(cc);
+	free(subject);
+	free(rpath);
+	return m;
+}
+
+int
+appendtofile(Message *m, char *part, char *base, int mbox, int f)
+{
+	char *folder, path[Pathlen];
+	int in, rv, rp;
+
+	in = open(extendp(m->path, part), OREAD);
+	if(in == -1){
+		dissappeared();
+		return -1;
+	}
+	rp = 0;
+	if(*base == '/')
+		folder = base;
+	else if(!mbox){
+		snprint(path, sizeof path, "%s/%s", wd, base);
+		folder = path;
+		rp = 1;
+	}else if(f)
+		folder = ffoldername(mbpath, user, base);
+	else
+		folder = foldername(mbpath, user, base);
+	if(folder == nil)
+		return -1;
+	if(mbox)
+		rv = fappendfolder(0, 0, folder, in);
+	else
+		rv = fappendfile(m->from, folder, in);
+	close(in);
+	if(rv >= 0){
+		eprint("!saved in %s\n", rp? base: folder);
+		setflags(m, "S");
+	}else
+		eprint("!error %r\n");
+	return rv;
+}
+
+Message*
+scmd(Cmd *c, Message *m)
+{
+	char *file;
+
+	switch(c->an){
+	case 1:
+		file = "stored";
+		break;
+	case 2:
+		file = c->av[1];
+		break;
+	default:
+		eprint("!usage: s filename\n");
+		return nil;
+	}
+
+	if(appendtofile(m, "rawunix", file, 1, 0) < 0)
+		return nil;
+	return m;
+}
+
+Message*
+wcmd(Cmd *c, Message *m)
+{
+	char *file;
+
+	switch(c->an){
+	case 2:
+		file = c->av[1];
+		break;
+	case 1:
+		if(*m->filename == 0){
+			eprint("!usage: w filename\n");
+			return nil;
+		}
+		file = strrchr(m->filename, '/');
+		if(file != nil)
+			file++;
+		else
+			file = m->filename;
+		break;
+	default:
+		eprint("!usage: w filename\n");
+		return nil;
+	}
+
+	if(appendtofile(m, "body", file, 0, 0) < 0)
+		return nil;
+	return m;
+}
+
+typedef struct Xtab Xtab;
+struct Xtab {
+	char	*a;
+	char	*b;
+};
+Xtab	*xtab;
+int	nxtab;
+
+void
+loadxfrom(int fd)
+{
+	char *f[3], *s, *p;
+	int n, a, inc;
+	Biobuf b;
+	Xtab *x;
+
+	Binit(&b, fd, OREAD);
+	a = 0;
+	inc = 100;
+	for(; s = Brdstr(&b, '\n', 1);){
+		if(p = strchr(s, '#'))
+			*p = 0;
+		n = tokenize(s, f, nelem(f));
+		if(n != 2){
+			free(s);
+			continue;
+		}
+		if(nxtab == a){
+			a += inc;
+			xtab = realloc(xtab, a*sizeof *xtab);
+			if(xtab == nil)
+				sysfatal("realloc: %r");
+			inc *= 2;
+		}
+		for(x = xtab+nxtab; x > xtab && strcmp(x[-1].a, f[0]) > 0; x--)
+			x[0] = x[-1];
+		x->a = f[0];
+		x->b = f[1];
+		nxtab++;
+	}
+}
+
+char*
+frombox(char *from)
+{
+	char *s;
+	int n, m, fd;
+	Xtab *t, *p;
+	static int once;
+
+	if(once == 0){
+		once = 1;
+		s = foldername(mbpath, user, "fromtab-");
+		fd = open(s, OREAD);
+		if(fd != -1)
+			loadxfrom(fd);
+		close(fd);
+	}
+	t = xtab;
+	n = nxtab;
+	while(n > 1) {
+		m = n/2;
+		p = t + m;
+		if(strcmp(from, p->a) > 0){
+			t = p;
+			n = n - m;
+		} else
+			n = m;
+	}
+	if(n && strcmp(from, t->a) == 0)
+		return t->b;
+	return from;
+}
+
+Message*
+fcmd(Cmd*, Message *m)
+{
+	char *f;
+
+	f = frombox(m->from);
+	if(appendtofile(m, "rawunix", f, 1, 1) < 0)
+		return nil;
+	return m;
+}
+
+Message*
+fqcmd(Cmd*, Message *m)
+{
+	char *f;
+
+	f = frombox(m->from);
+	Bprint(&out, "! %s\n", f);
+	return m;
+}
+
+void
+system(char *cmd, char **av, int in)
+{
+	switch(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);
+		eprint("!couldn't exec %s\n", cmd);
+		exits(0);
+	default:
+		if(in >= 0)
+			close(in);
+		while(waitpid() < 0){
+			if(!interrupted)
+				break;
+			interrupted = 0;
+			continue;
+		}
+		break;
+	}
+}
+
+Message*
+bangcmd(Cmd *c, Message *m)
+{
+	char *av[4];
+
+	av[0] = "rc";
+	av[1] = "-c";
+	av[2] = c->cmdline;
+	av[3] = 0;
+	system("/bin/rc", av, -1);
+//	Bprint(&out, "!\n");
+	return m;
+}
+
+Message*
+xpipecmd(Cmd *c, Message *m, char *part)
+{
+	char *av[4];
+	int fd;
+
+	if(c->an < 2){
+		eprint("!usage: | cmd\n");
+		return nil;
+	}
+
+	fd = open(extendp(m->path, part), OREAD);
+	if(fd < 0){
+		dissappeared();
+		return nil;
+	}
+
+	av[0] = "rc";
+	av[1] = "-c";
+	av[2] = c->cmdline;
+	av[3] = 0;
+	system("/bin/rc", av, fd);	/* system closes fd */
+//	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");
+}
+
+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)
+	if(fprint(fd, "close %q", mbname) == -1)
+		eprint("!close %q: %r", mbname);
+
+	close(fd);
+}
+
+static char*
+chop(char *s, int c)
+{
+	char *p;
+
+	p = strrchr(s, c);
+	if(p != nil && p > s) {
+		*p = 0;
+		return p - 1;
+	}
+	return 0;
+}
+
+/* sometimes opens the file (or open mbox) intended. */
+int
+switchmb(char *mb, int singleton)
+{
+	char *p, *e, pbuf[Pathlen], buf[Pathlen], file[Pathlen];
+	int fd, abs;
+
+	closemb();
+	abs = 0;
+	if(mb == nil)
+		mb = "mbox";
+	if(strcmp(mb, ".") == 0)	/* botch */
+		mb = homewd;
+	if(*mb == '/' || strncmp(mb, "./", 2) == 0 || strncmp(mb, "../", 3) == 0){
+		snprint(file, sizeof file, "%s", mb);
+		abs = 1;
+	}else
+		snprint(file, sizeof file, "/mail/fs/%s", mb);
+	if(singleton){
+		if(chop(file, '/') == nil || (p = strrchr(file, '/')) == nil || p - file < 2){
+			eprint("!bad mbox name\n");
+			return -1;
+		}
+		mboxpathbuf(pbuf, sizeof pbuf, user, "mbox");
+		snprint(mbname, sizeof mbname, "%s", p + 1);
+	}else if(abs || access(file, 0) < 0){
+		fd = open("/mail/fs/ctl", ORDWR);
+		if(fd < 0)
+			sysfatal("can't open /mail/fs/ctl: %r");
+		p = pbuf;
+		e = pbuf + sizeof pbuf;
+		if(abs && *file != '/')
+			seprint(p, e, "%s/%s", getwd(buf, sizeof buf), mb);
+		else if(abs)
+			seprint(p, e, "%s", mb);
+		else
+			mboxpathbuf(pbuf, sizeof pbuf, user, mb);
+		/* make up a handle to use when talking to fs */
+		if((p = strrchr(mb, '/')) == nil)
+			p = mb - 1;
+		snprint(mbname, sizeof mbname, "%s%ld", p + 1, time(0));
+		if(fprint(fd, "open %q %q", pbuf, mbname) < 0){
+			eprint("!can't open %q %q: %r\n", pbuf, mbname);
+			return -1;
+		}
+		close(fd);
+		didopen = 1;
+	}else{
+		mboxpathbuf(pbuf, sizeof pbuf, user, mb);
+		strcpy(mbname, mb);
+	}
+
+	snprint(root, sizeof root, "/mail/fs/%s", mbname);
+	if(getwd(wd, sizeof wd) == nil)
+		wd[0] = 0;
+	if(!singleton && chdir(root) >= 0)
+		strcpy(root, ".");
+	rootlen = strlen(root);
+	snprint(mbpath, sizeof mbpath, "%s", pbuf);
+	memset(&mbqid, 0, sizeof mbqid);
+	mbvers++;
+
+	return 0;
+}
+
+char*
+rooted(char *s)
+{
+	static char buf[Pathlen];
+
+	if(strcmp(root, ".") != 0)
+		return s;
+	snprint(buf, sizeof buf, "/mail/fs/%s/%s", mbname, s);
+	return buf;
+}
+
+int
+plumb(Message *m, Ctype *cp)
+{
+	char *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 *pm, 1);
+	pm->src = strdup("mail");
+	if(*cp->plumbdest)
+		pm->dst = strdup(cp->plumbdest);
+	pm->type = strdup("text");
+	pm->ndata = -1;
+	s = rooted(extendp(m->path, "body"));
+	if(cp->ext != nil)
+		pm->data  = smprint("%s.%s", s, cp->ext);
+	else
+		pm->data  = strdup(s);
+	plumbsend(fd, pm);
+	plumbfree(pm);
+	return 0;
+}
+
+void
+regerror(char*)
+{
+}
+
+void
+exitfs(char *rv)
+{
+	if(startedfs)
+		unmount(nil, "/mail/fs");
+	chdir(homewd);			/* prof */
+	exits(rv);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/pop3/mkfile
@@ -1,0 +1,9 @@
+</$objtype/mkfile
+
+TARG=pop3
+LIB=../common/libcommon.a$O
+OFILES=pop3.$O
+
+</sys/src/cmd/mkone
+<../mkupas
+CFLAGS=$CFLAGS -I../common
--- /dev/null
+++ b/sys/src/cmd/upas/pop3/pop3.c
@@ -1,0 +1,836 @@
+#include "common.h"
+#include <ctype.h>
+#include <auth.h>
+#include <libsec.h>
+
+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] "
+		"[-r remote] [-t cert]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int fd;
+	char *arg, cmdbuf[1024];
+	Cmd *c;
+	NetConnInfo *n;
+
+	rfork(RFNAMEG);
+	Binit(&in, 0, OREAD);
+	Binit(&out, 1, OWRITE);
+
+	tmfmtinstall();
+
+	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)
+	if(n = getnetconninfo(0, 0)){
+		peeraddr = strdup(n->rsys);
+		freenetconninfo(n);
+	}
+
+	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; i<nd; i++){
+		m = &msg[nmsg];
+		m->upasnum = 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(Blinelen(b) == 0)
+					break;
+			}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 = &in;
+
+	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; i++){
+			if(msg[i].deleted)
+				continue;
+			sendcrnl("%d %d", i+1, msg[i].bytes);
+		}
+		sendcrnl(".");
+	}else{
+		n = atoi(arg)-1;
+		if(n < 0 || n >= nmsg || msg[n].deleted)
+			return senderr("no such message");
+		sendok("%d %d", n+1, msg[n].bytes);
+	}
+	return 0;
+}
+
+static int
+noopcmd(char*)
+{
+	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; i++)
+		if(msg[i].deleted)
+			fmtprint(&f, " %d", msg[i].upasnum);
+	s = fmtstrflush(&f);
+	if(strcmp(s, "delete mbox") != 0){	/* must have something to delete */
+		if((fd = open("../ctl", OWRITE)) < 0){
+			senderr("open ctl to delete messages: %r");
+			return;
+		}
+		if(write(fd, s, strlen(s)) < 0){
+			senderr("error deleting messages: %r");
+			return;
+		}
+	}
+	sendok("");
+}
+
+static int
+synccmd(char*)
+{
+	_synccmd(nil);
+	return 0;
+}
+
+static int
+quitcmd(char*)
+{
+	synccmd(nil);
+	exits(nil);
+	return 0;
+}
+
+static int
+retrcmd(char *arg)
+{
+	int n;
+	Biobuf *b;
+	char buf[40], *p;
+
+	if(*arg == 0)
+		return senderr("RETR requires a message number");
+	n = atoi(arg)-1;
+	if(n < 0 || n >= 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; i++){
+		if(msg[i].deleted){
+			msg[i].deleted = 0;
+			totalmsgs++;
+			totalbytes += msg[i].bytes;
+		}
+	}
+	return sendok("");
+}
+
+static int
+statcmd(char*)
+{
+	return sendok("%d %d", totalmsgs, totalbytes);
+}
+
+static int
+trace(char *fmt, ...)
+{
+	va_list arg;
+	int n;
+
+	va_start(arg, fmt);
+	n = vfprint(2, fmt, arg);
+	va_end(arg);
+	return n;
+}
+
+static int
+stlscmd(char*)
+{
+	int fd;
+	TLSconn conn;
+
+	if(didtls)
+		return senderr("tls already started");
+	if(!tlscert)
+		return senderr("don't have any tls credentials");
+	sendok("");
+	Bflush(&out);
+
+	memset(&conn, 0, sizeof conn);
+	conn.cert = tlscert;
+	conn.certlen = ntlscert;
+	if(debug)
+		conn.trace = trace;
+	fd = tlsServer(0, &conn);
+	if(fd < 0)
+		sysfatal("tlsServer: %r");
+	dup(fd, 0);
+	dup(fd, 1);
+	close(fd);
+	Binit(&in, 0, OREAD);
+	Binit(&out, 1, OWRITE);
+	didtls = 1;
+	return 0;
+}
+
+static int
+topcmd(char *arg)
+{
+	int done, i, lines, n;
+	char buf[40], *p;
+	Biobuf *b;
+
+	if(*arg == 0)
+		return senderr("TOP requires a message number");
+	n = atoi(arg)-1;
+	if(n < 0 || n >= 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<lines; i++){
+		p = Brdstr(b, '\n', 1);
+		if(p == nil)
+			break;
+		if(p[0]=='.')
+			Bwrite(&out, ".", 1);
+		Bwrite(&out, p, strlen(p));
+		Bwrite(&out, "\r\n", 2);
+		free(p);
+	}
+	sendcrnl(".");
+	Bterm(b);
+	return 0;
+}
+
+static int
+uidlcmd(char *arg)
+{
+	int n;
+
+	if(*arg==0){
+		sendok("");
+		for(n=0; n<nmsg; n++){
+			if(msg[n].deleted)
+				continue;
+			sendcrnl("%d %s", n+1, msg[n].digest);
+		}
+		sendcrnl(".");
+	}else{
+		n = atoi(arg)-1;
+		if(n < 0 || n >= 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[Pathlen];
+char box[Pathlen];
+char cbox[Pathlen];
+
+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;
+
+	*user = 0;
+	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){
+		sleep(15*1000);
+		senderr("you are not expected to understand this");	/* pop3 attack. */
+		exits("");
+	}
+	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;
+	static ulong delaysecs = 5;
+
+	chs->user = user;
+	chs->resp = response;
+	chs->nresp = strlen(response);
+	if((ai = auth_response(chs)) == nil){
+		if(tries >= 20){
+			senderr("authentication failed: %r; server exiting");
+			exits(nil);
+		}
+		if(++tries == 3)
+			syslog(0, "pop3", "likely password guesser from %s",
+				peeraddr);
+		delaysecs *= 2;
+		if (delaysecs > 30*60)
+			delaysecs = 30*60;		/* half-hour max. */
+		sleep(delaysecs * 1000); /* prevent beating on our auth server */
+		return senderr("authentication failed");
+	}
+
+	if(auth_chuid(ai, nil) < 0){
+		senderr("chuid failed: %r; server exiting");
+		exits(nil);
+	} else {	/* chown network connection */
+		Dir nd;
+		nulldir(&nd);
+		nd.mode = 0660;
+		nd.uid = ai->cuid;
+		dirfwstat(Bfildes(&in), &nd);
+	}
+	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(*user == 0){
+		senderr("inscrutable phase error");	// pop3 attack.
+		sleep(15*1000);
+		exits("");
+	}
+
+	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){
+		senderr("i before e except after c");	// pop3 attack.
+		sleep(15*1000);
+		exits("");
+	}
+	return dologin(resp);
+}
+
--- /dev/null
+++ b/sys/src/cmd/upas/q/mkfile
@@ -1,0 +1,15 @@
+</$objtype/mkfile
+
+TARG=\
+	qer\
+	runq\
+
+LIB=../common/libcommon.a$O
+OFILES=
+HFILES=\
+	../common/common.h\
+	../common/sys.h\
+
+</sys/src/cmd/mkmany
+<../mkupas
+CFLAGS=$CFLAGS -I../common
--- /dev/null
+++ b/sys/src/cmd/upas/q/qer.c
@@ -1,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[ERRMAX];
+	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", 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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/q/runq.c
@@ -1,0 +1,689 @@
+#include "common.h"
+#include <ctype.h>
+
+typedef struct Job Job;
+typedef struct Wdir Wdir;
+typedef struct Wpid Wpid;
+
+struct Wdir {
+	Dir	*d;
+	int	nd;
+	char	*name;
+};
+
+struct Job {
+	Job	*next;
+	int	pid;
+	int	ac;
+	int	dfd;
+	char	**av;
+	char	*buf;	/* backing for av */
+	Wdir	*wdir;	/* work dir */
+	Dir	*dp;	/* not owned */
+	Mlock	*l;
+	Biobuf	*b;
+};
+
+void	doalldirs(void);
+void	dodir(char*);
+Job*	dofile(Wdir*, Dir*);
+Job*	donefile(Job*, Waitmsg*);
+void	freejob(Job*);
+void	rundir(char*);
+char*	file(char*, char);
+void	warning(char*, void*);
+void	error(char*, void*);
+int	returnmail(char**, Wdir*, char*, char*);
+void	logit(char*, Wdir*, char*, char**);
+void	doload(int);
+
+char	*cmd;
+char	*root;
+int	debug;
+int	giveup = 2*24*60*60;
+int	limit;
+Wpid	*waithd;
+Wpid	*waittl;
+
+char *runqlog = "runq";
+
+char	**badsys;		/* array of recalcitrant systems */
+int	nbad;
+int	njob = 1;		/* number of concurrent jobs to invoke */
+int	Eflag;			/* ignore E.xxxxxx dates */
+int	Rflag;			/* no giving up, ever */
+int	aflag;			/* do all dirs */
+
+void
+usage(void)
+{
+	fprint(2, "usage: runq [-dE] [-q dir] [-l load] [-t time] [-r nfiles] [-n nprocs] q-root cmd\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *qdir;
+
+	qdir = 0;
+
+	ARGBEGIN{
+	case 'E':
+		Eflag++;
+		break;
+	case 'R':	/* no giving up -- just leave stuff in the queue */
+		Rflag++;
+		break;
+	case 'd':
+		debug++;
+		break;
+	case 'r':
+		limit = atoi(EARGF(usage()));
+		break;
+	case 't':
+		giveup = 60*60*atoi(EARGF(usage()));
+		break;
+	case 'q':
+		qdir = EARGF(usage());
+		break;
+	case 'a':
+		aflag++;
+		break;
+	case 'n':
+		njob = atoi(EARGF(usage()));
+		if(njob == 0)
+			usage();
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	if(argc != 2)
+		usage();
+
+	if(!aflag && qdir == nil){
+		qdir = getuser();
+		if(qdir == nil)
+			error("unknown user", 0);
+	}
+	root = argv[0];
+	cmd = argv[1];
+
+	if(chdir(root) < 0)
+		error("can't cd to %s", root);
+
+	if(aflag)
+		doalldirs();
+	else
+		dodir(qdir);
+	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);
+		remove(name);
+		return 1;
+	}
+	return 0;
+}
+
+/*
+ *  run all user directories, must be bootes (or root on unix) to do this
+ */
+void
+doalldirs(void)
+{
+	Dir *db;
+	int fd;
+	long i, n;
+
+
+	if((fd = open(".", OREAD)) == -1){
+		warning("opening %s", root);
+		return;
+	}
+	if((n = dirreadall(fd, &db)) == -1){
+		warning("reading %s: ", root);
+		close(fd);
+		return;
+	}
+	for(i=0; i<n; i++){
+		if((db[i].qid.type & QTDIR) == 0)
+			continue;
+		if(emptydir(db[i].name))
+			continue;
+		dodir(db[i].name);
+	}
+	close(fd);
+	free(db);
+}
+
+/*
+ *  cd to a user directory and run it
+ */
+void
+dodir(char *name)
+{
+	if(chdir(name) < 0){
+		warning("cd to %s", name);
+		return;
+	}
+	if(debug)
+		fprint(2, "running %s\n", name);
+	rundir(name);
+	chdir("..");
+}
+
+/*
+ *  run the current directory
+ */
+void
+rundir(char *name)
+{
+	int nlive, fidx, fd, found;
+	Job *hd, *j, **p;
+	Waitmsg *w;
+	Mlock *l;
+	Wdir wd;
+
+	fd = open(".", OREAD);
+	if(fd == -1){
+		warning("reading %s", name);
+		return;
+	}
+	if((l = syslock("./rundir")) == nil){
+		warning("locking %s", name);
+		close(fd);
+		return;
+	}
+	fidx= 0;
+	hd = nil;
+	nlive = 0;
+	wd.name = name;
+	wd.nd = dirreadall(fd, &wd.d);
+	while(nlive > 0 ||  fidx< wd.nd){
+		for(; fidx< wd.nd && nlive < njob; fidx++){
+			if(strncmp(wd.d[fidx].name, "C.", 2) != 0)
+				continue;
+			if((j = dofile(&wd, &wd.d[fidx])) == nil){
+				if(debug) fprint(2, "skipping %s: %r\n", wd.d[fidx].name);
+				continue;
+			}
+			nlive++;
+			j->next = hd;
+			hd = j;
+		}
+		/* nothing to do */
+		if(nlive == 0)
+			break;
+rescan:
+		if((w = wait()) == nil){
+			syslog(0, "runq", "wait error: %r");
+			break;
+		}
+		found = 0;
+		for(p = &hd; *p != nil; p = &(*p)->next){
+			if(w->pid == (*p)->pid){
+				*p = donefile(*p, w);
+				found++;
+				nlive--;
+				break;
+			}
+		}
+		free(w);
+		if(!found){
+			syslog(0, runqlog, "wait: pid not in job list");
+			goto rescan;
+		}
+	}
+	assert(hd == nil);
+	free(wd.d);
+	close(fd);
+	sysunlock(l);
+}
+
+/*
+ *  free files matching name in the current directory
+ */
+void
+remmatch(Wdir *w, char *name)
+{
+	long i;
+
+	syslog(0, runqlog, "removing %s/%s", w->name, name);
+	for(i=0; i<w->nd; i++){
+		if(strcmp(&w->d[i].name[1], &name[1]) == 0)
+			remove(w->d[i].name);
+	}
+
+	/* error file (may have) appeared after we read the directory */
+	/* stomp on data file in case of phase error */
+	remove(file(name, 'D'));
+	remove(file(name, 'E'));
+}
+
+/*
+ *  like trylock, but we've already got the lock on fd,
+ *  and don't want an L. lock file.
+ */
+static Mlock *
+keeplockalive(char *path, int fd)
+{
+	char buf[1];
+	Mlock *l;
+
+	l = malloc(sizeof(Mlock));
+	if(l == 0)
+		return 0;
+	l->fd = fd;
+	snprint(l->name, sizeof l->name, "%s", path);
+
+	/* fork process to keep lock alive until sysunlock(l) */
+	switch(l->pid = rfork(RFPROC|RFNOWAIT)){
+	default:
+		break;
+	case 0:
+		fd = l->fd;
+		for(;;){
+			sleep(1000*60);
+			if(pread(fd, buf, 1, 0) < 0)
+				break;
+		}
+		_exits(0);
+	}
+	return l;
+}
+
+/*
+ *  Launch trying a message, returning a job
+ *  tracks the running pid.
+ */
+Job*
+dofile(Wdir *w, Dir *dp)
+{
+	int dtime, efd, i, etime;
+	Job *j;
+	Dir *d;
+	char *cp;
+
+	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(w, dp->name);
+		return nil;
+	}
+	if(dp->length == 0){
+		if(time(0)-dp->mtime > 15*60){
+			syslog(0, runqlog, "empty ctl file for %s", dp->name);
+			remmatch(w, dp->name);
+		}
+		return nil;
+	}
+	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){
+				werrstr("early retry");
+				return nil;
+			}
+		} else {
+			/* after the first hour, try once an hour */
+			if(time(0) - etime < 60*60){
+				werrstr("early retry");
+				return nil;
+			}
+		}
+	}
+
+	/*
+	 *  open control and data
+	 */
+	j = malloc(sizeof(Job));
+	if(j == nil)
+		return nil;
+	memset(j, 0, sizeof(Job));
+	j->dp = dp;
+	j->dfd = -1;
+	j->b = sysopen(file(dp->name, 'C'), "rl", 0660);
+	if(j->b == 0)
+		goto done;
+	j->dfd = open(file(dp->name, 'D'), OREAD);
+	if(j->dfd < 0)
+		goto done;
+
+	/*
+	 *  make arg list
+	 *	- read args into (malloc'd) buffer
+	 *	- malloc a vector and copy pointers to args into it
+	 */
+	j->wdir = w;
+	j->buf = malloc(dp->length+1);
+	if(j->buf == nil){
+		warning("buffer allocation", 0);
+		freejob(j);
+		return nil;
+	}
+	if(Bread(j->b, j->buf, dp->length) != dp->length){
+		warning("reading control file %s\n", dp->name);
+		freejob(j);
+		return nil;
+	}
+	j->buf[dp->length] = 0;
+	j->av = malloc(2*sizeof(char*));
+	if(j->av == 0){
+		warning("argv allocation", 0);
+		freejob(j);
+		return nil;
+	}
+	for(j->ac = 1, cp = j->buf; *cp; j->ac++){
+		while(isspace(*cp))
+			*cp++ = 0;
+		if(*cp == 0)
+			break;
+
+		j->av = realloc(j->av, (j->ac+2)*sizeof(char*));
+		if(j->av == 0){
+			warning("argv allocation", 0);
+		}
+		j->av[j->ac] = cp;
+		while(*cp && !isspace(*cp)){
+			if(*cp++ == '"'){
+				while(*cp && *cp != '"')
+					cp++;
+				if(*cp)
+					cp++;
+			}
+		}
+	}
+	j->av[0] = cmd;
+	j->av[j->ac] = 0;
+
+	if(!Eflag &&time(0) - dtime > giveup){
+		if(returnmail(j->av, w, dp->name, "Giveup") != 0)
+			logit("returnmail failed", w, dp->name, j->av);
+		remmatch(w, dp->name);
+		goto done;
+	}
+
+	for(i = 0; i < nbad; i++){
+		if(j->ac > 3 && strcmp(j->av[3], badsys[i]) == 0){
+			werrstr("badsys: %s", j->av[3]);
+			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.
+ 	 */
+	j->l = keeplockalive(file(dp->name, 'C'), Bfildes(j->b));
+	if(j->l == nil){
+		warning("lock file", 0);
+		goto done;
+	}
+
+	/*
+	 *  transfer
+	 */
+	j->pid = fork();
+	switch(j->pid){
+	case -1:
+		sysunlock(j->l);
+		sysunlockfile(Bfildes(j->b));
+		syslog(0, runqlog, "out of procs");
+		exits(0);
+	case 0:
+		if(debug) {
+			fprint(2, "Starting %s\n", cmd);
+			for(i = 0; j->av[i]; i++)
+				fprint(2, " %s", j->av[i]);
+			fprint(2, "\n");
+		}
+		logit("execing", w, dp->name, j->av);
+		close(0);
+		dup(j->dfd, 0);
+		close(j->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, j->av);
+		error("can't exec %s", cmd);
+		break;
+	default:
+		return j;
+	}
+done:
+	freejob(j);
+	return nil;
+}
+
+/*
+ * Handle the completion of a job.
+ * Wait for the pid, check its status,
+ * and then pop the job off the list.
+ * Return the next running job.
+ */
+Job*
+donefile(Job *j, Waitmsg *wm)
+{
+	Job *n;
+
+	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(j->av, j->wdir, j->dp->name, wm->msg) != 0)
+				logit("returnmail failed", j->wdir, j->dp->name, j->av);
+			remmatch(j->wdir, j->dp->name);
+		} else {
+			/* add sys to bad list and try again later */
+			nbad++;
+			badsys = realloc(badsys, nbad*sizeof(char*));
+			badsys[nbad-1] = strdup(j->av[3]);
+		}
+	} else {
+		/* it worked remove the message */
+		remmatch(j->wdir, j->dp->name);
+	}
+	n = j->next;
+	freejob(j);
+	return n;
+}
+
+/*
+ * Release resources associated with
+ * a job.
+ */
+void
+freejob(Job *j)
+{
+	if(j->b != nil){
+		sysunlockfile(Bfildes(j->b));
+		Bterm(j->b);
+	}
+	if(j->dfd != -1)
+		close(j->dfd);
+	if(j->l != nil)
+		sysunlock(j->l);
+	free(j->buf);
+	free(j->av);
+	free(j);
+}
+
+
+/*
+ *  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, Wdir *w, char *name, char *msg)
+{
+	char buf[256], attachment[Pathlen], *sender;
+	int fd, pfd[2];
+	long n;
+	String *s;
+
+	if(av[1] == 0 || av[2] == 0){
+		logit("runq - dumping bad file", w, 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", w, name, av);
+		return 0;
+	}
+
+	if(pipe(pfd) < 0){
+		logit("runq - pipe failed", w, name, av);
+		return -1;
+	}
+
+	switch(rfork(RFFDG|RFPROC|RFENVG|RFNOWAIT)){
+	case -1:
+		logit("runq - fork failed", w, name, av);
+		return -1;
+	case 0:
+		logit("returning", w, 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);
+				return -1;
+			}
+		}
+		close(fd);
+	}
+	close(pfd[1]);
+	return 0;
+}
+
+/*
+ *  print a warning and continue
+ */
+void
+warning(char *f, void *a)
+{
+	char err[ERRMAX];
+	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[ERRMAX];
+	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, Wdir *w, char *file, char **av)
+{
+	int n, m;
+	char buf[256];
+
+	n = snprint(buf, sizeof(buf), "%s/%s: %s", w->name, 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);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/qfrom/mkfile
@@ -1,0 +1,7 @@
+</$objtype/mkfile
+
+TARG=qfrom
+OFILES=qfrom.$O
+
+</sys/src/cmd/mkone
+<../mkupas
--- /dev/null
+++ b/sys/src/cmd/upas/qfrom/qfrom.c
@@ -1,0 +1,63 @@
+/*
+ * quote from lines without messing with character encoding.
+ *	(might rather just undo the character encoding and use sed.)
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+void
+qfrom(int fd)
+{
+	Biobuf b, bo;
+	char *s;
+	int l;
+
+	if(Binit(&b, fd, OREAD) == -1)
+		sysfatal("Binit: %r");
+	if(Binit(&bo, 1, OWRITE) == -1)
+		sysfatal("Binit: %r");
+	
+	while(s = Brdstr(&b, '\n', 0)){
+		l = Blinelen(&b);
+		if(l >= 5)
+		if(memcmp(s, "From ", 5) == 0)
+			Bputc(&bo, ' ');
+		Bwrite(&bo, s, l);
+		free(s);
+	}
+	Bterm(&b);
+	Bterm(&bo);
+}
+	
+void
+usage(void)
+{
+	fprint(2, "usage: qfrom [files...]\n");
+	exits("");
+}
+
+void
+main(int argc, char **argv)
+{
+	int fd;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+	
+	if(*argv == 0){
+		qfrom(0);
+		exits("");
+	}
+	for(; *argv; argv++){
+		fd = open(*argv, OREAD);
+		if(fd == -1)
+			sysfatal("open: %r");
+		qfrom(fd);
+		close(fd);
+	}
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/scanmail/common.c
@@ -1,0 +1,670 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <regexp.h>
+#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 (<!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, 
+		utfnlen(p, m->sp-p), p,
+		utfnlen(m->sp, m->ep-m->sp), m->sp,
+		utfnlen(m->ep, 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,
+};
--- /dev/null
+++ b/sys/src/cmd/upas/scanmail/mkfile
@@ -1,0 +1,20 @@
+</$objtype/mkfile
+
+TARG=\
+	scanmail\
+	testscan
+
+LIB=../common/libcommon.a$O
+
+OFILES=common.$O
+
+HFILES=\
+	spam.h\
+	../common/sys.h\
+
+</sys/src/cmd/mkmany
+<../mkupas
+CFLAGS=$CFLAGS -I../common
+
+scanmail.$O: scanmail.c
+	$CC $CFLAGS scanmail.c
--- /dev/null
+++ b/sys/src/cmd/upas/scanmail/scanmail.c
@@ -1,0 +1,476 @@
+#include "common.h"
+#include <regexp.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, "usage: scanmail [-cdhnstv] [-p pattern] [-q queuename] sender dest sys\n");
+	exits("usage");
+}
+
+void
+regerror(char *s)
+{
+	fprint(2, "scanmail: %s\n", s);
+}
+
+void *
+Malloc(long n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		sysfatal("malloc: %r");
+	return p;
+}
+
+void*
+Realloc(void *p, ulong n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		sysfatal("malloc: %r");
+	setrealloctag(p, getcallerpc(&p));
+	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*));
+	snprint(patfile, sizeof patfile, "%s/patterns", UPASLIB);
+	snprint(linefile, sizeof linefile, "%s/lines", UPASLOG);
+	snprint(holdqueue, sizeof holdqueue, "%s/queue.hold", SPOOL);
+	snprint(copydir, sizeof 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;
+	Tm tm;
+	ulong h;
+	char buf[512];
+	Biobuf *b;
+	char *cp, mon[8], day[4];
+
+	tmnow(&tm, nil);
+	snprint(mon, sizeof(mon), "%τ", tmfmt(&tm, "MMM"));
+	snprint(day, sizeof(day), "%τ", tmfmt(&tm, "D"));
+	snprint(buf, sizeof buf, "%s/queue.dump/%s%s", SPOOL, mon, day);
+	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();
+		seprint(cp, buf+sizeof buf, "/%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();
+		snprint(buf, sizeof 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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/scanmail/spam.h
@@ -1,0 +1,61 @@
+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*);
--- /dev/null
+++ b/sys/src/cmd/upas/scanmail/testscan.c
@@ -1,0 +1,211 @@
+#include "common.h"
+#include <regexp.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, "usage: testscan -avd [-p pattern] ...\n");
+	exits("usage");
+}
+
+void *
+Malloc(long n)
+{
+	void *p;
+
+	p = malloc(n);
+	if(p == nil)
+		sysfatal("malloc: %r");
+	setmalloctag(p, getcallerpc(&n));
+	return p;
+}
+
+void*
+Realloc(void *p, ulong n)
+{
+	p = realloc(p, n);
+	if(p == nil)
+		sysfatal("malloc: %r");
+	setrealloctag(p, getcallerpc(&p));
+	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 <REGEXP>\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;
+
+	snprint(patfile, sizeof 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':
+		snprint(patfile, sizeof patfile, "%s", EARGF(usage()));
+		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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/authorize.c
@@ -1,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), 0, 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);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/bind.c
@@ -1,0 +1,131 @@
+#include "common.h"
+#include "send.h"
+
+/* 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, found;
+
+	found = 0;
+	len = strlen(system);
+	while(addr = strchr(addr, '!'))
+		if (!strncmp(++addr, system, len)
+		 && addr[len] == '!' && ++found == 4)
+			return 1;
+	return 0;
+}
+
+
+/* bind the destinations to the commands to be executed */
+dest *
+up_bind(dest *destp, message *mp, int checkforward)
+{
+	int i, li;
+	dest *list[2], *bound, *dp;
+
+	bound = nil;
+	list[0] = destp;
+	list[1] = nil;
+
+	/*
+	 *  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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/cat_mail.c
@@ -1,0 +1,41 @@
+#include "common.h"
+#include "send.h"
+
+/* dispose of local addresses */
+int
+cat_mail(dest *dp, message *mp)
+{
+	char *rcvr, *cp, *s;
+	String *ss;
+	Biobuf *b;
+	int e;
+
+	ss = unescapespecial(s_clone(dp->repl1));
+	s = s_to_c(ss);
+	if (flagn) {
+		if(!flagx)
+			print("upas/mbappend %s\n", s);
+		else
+			print("%s\n", s_to_c(dp->addr));
+		s_free(ss);
+		return 0;
+	}
+	/* avoid lock errors */
+	if(strcmp(s, "/dev/null") == 0){
+		s_free(ss);
+		return(0);
+	}
+	b = openfolder(s, time(0));
+	s_free(ss);
+	if(b == nil)
+		return refuse(dp, mp, "mail file cannot be created", 0, 0);
+	e = m_print(mp, b, 0, 1) == -1 || Bprint(b, "\n") == -1;
+	e |= closefolder(b);
+	if(e != 0)
+		return refuse(dp, mp, "error writing mail file", 0, 0);
+	rcvr = s_to_c(dp->addr);
+	if(cp = strrchr(rcvr, '!'))
+		rcvr = cp+1;
+	logdelivery(dp, rcvr, mp);
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/dest.c
@@ -1,0 +1,262 @@
+#include "common.h"
+#include "send.h"
+
+/* exports */
+dest *dlist;
+
+dest*
+d_new(String *addr)
+{
+	dest *dp;
+
+	dp = (dest *)mallocz(sizeof(dest), 1);
+	if (dp == 0)
+		sysfatal("malloc: %r");
+	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;
+}
+
+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'. */
+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'. */
+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'. */
+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.
+ */
+void
+d_same_insert(dest **listp, dest *new)
+{
+	dest *dp;
+	int len;
+
+	if(new->status == d_pipe || new->status == d_cat) {
+		len = 0;
+		if(new->repl2)
+			len = strlen(s_to_c(new->repl2));
+		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);
+		}
+		if(s_to_c(new->repl1))
+			new->nchar = strlen(s_to_c(new->repl1)) + len + 1;
+		else
+			new->nchar = 0;
+	}
+	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.
+ */
+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);
+}
+
+
+#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;
+}
+
+/* expand a String of destinations into a linked list of destiniations */
+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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/filter.c
@@ -1,0 +1,103 @@
+#include "common.h"
+#include "send.h"
+#include <regexp.h>
+
+Biobuf	bin;
+int	flagn;
+int	flagx;
+int	rmail;
+int	tflg;
+char	*subjectarg;
+
+char*
+findbody(char *p)
+{
+	if(*p == '\n')
+		return p;
+
+	while(*p){
+		if(*p == '\n' && *(p+1) == '\n')
+			return p+1;
+		p++;
+	}
+	return p;
+}
+
+int
+refuse(dest*, message *, char *cp, int, int)
+{
+	fprint(2, "%s\n", cp);
+	exits("error");
+	return 0;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: upas/filter [-nbh] rcvr mailbox [regexp file] ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char *argv[])
+{
+	char *cp, file[Pathlen];
+	int i, header, body;
+	message *mp;
+	dest *dp;
+	Reprog *p;
+	Resub match[10];
+
+	header = body = 0;
+	ARGBEGIN {
+	case 'n':
+		flagn = 1;
+		break;
+	case 'h':
+		header = 1;
+		break;
+	case 'b':
+		header = 1;
+		body = 1;
+		break;
+	default:
+		usage();
+	} ARGEND
+
+	Binit(&bin, 0, OREAD);
+	if(argc < 2)
+		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);
+	}
+
+	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].sp >= cp)
+				continue;
+			regsub(argv[i+1], file, sizeof(file), match, 10);
+			break;
+		}
+	}
+	dp = d_new(s_copy(argv[0]));
+	dp->repl1 = s_copy(file);
+	if(cat_mail(dp, mp) != 0)
+		exits("fail");
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/gateway.c
@@ -1,0 +1,21 @@
+#include "common.h"
+#include "send.h"
+
+/*
+ *  Translate the last component of the sender address.  If the translation
+ *  yields the same address, replace the sender with its last component.
+ */
+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);
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/local.c
@@ -1,0 +1,184 @@
+#include "common.h"
+#include "send.h"
+
+static String*
+mboxpath(char *path, char *user, String *to)
+{
+	char buf[Pathlen];
+
+	mboxpathbuf(buf, sizeof buf, user, path);
+	return s_append(to, buf);
+}
+
+static void
+mboxfile(dest *dp, String *user, String *path, char *file)
+{
+	char *cp;
+
+	mboxpath(s_to_c(user), s_to_c(dp->addr), path);
+	cp = strrchr(s_to_c(path), '/');
+	if(cp)
+		path->ptr = cp+1;
+	else
+		path->ptr = path->base;
+	s_append(path, file);
+}
+
+/*
+ * BOTCH, BOTCH
+ * the problem is that we don't want to say a user exists
+ * just because the user has a mail box directory.  that
+ * precludes using mode bits to disable mailboxes.
+ *
+ * botch #1: we pretend like we know that there must be a
+ * corresponding file or directory /mail/box/$user[/folder]/mbox
+ * this is wrong, but we get away with this for local mail boxes.
+ *
+ * botch #2: since the file server and not the auth server owns
+ * groups, it's not possible to get groups right.  this means that
+ * a mailbox that only allows members of a group to post but
+ * not read wouldn't work.
+ */
+static uint accesstx[] = {
+[OREAD]	1<<2,
+[OWRITE]	1<<1,
+[ORDWR]	3<<1,
+[OEXEC]	1<<0
+};
+
+static int
+accessmbox(char *f, int m)
+{
+	int r, n;
+	Dir *d;
+
+	d = dirstat(f);
+	if(d == nil)
+		return -1;
+	n = 0;
+	if(m < nelem(accesstx))
+		n = accesstx[m];
+	if(d->mode & DMDIR)
+		n |= OEXEC;
+	r = (d->mode & n<<0) == n<<0;
+//	if(r == 0 && inlist(mygids(), d->gid) == 0)
+//		r = (d->mode & n<<3) == n<<3;
+	if(r == 0 && strcmp(getlog(), d->uid) == 0)
+		r = (d->mode & n<<6) == n<<6;
+	r--;
+	free(d);
+	return r;
+}
+
+/*
+ *  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();
+		mboxpath("mbox", 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(access(s_to_c(file), AEXEC) == 0){
+		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), "mbox");
+	if(accessmbox(s_to_c(file), OWRITE) != -1)
+		dp->status = d_cat;
+	else
+		dp->status = d_unknown;
+	s_free(file);
+	s_free(s);
+	return 0;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/log.c
@@ -1,0 +1,82 @@
+#include "common.h"
+#include "send.h"
+
+/* log mail delivery */
+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 */
+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 */
+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));
+
+	snprint(buf, sizeof 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<ep) {
+		*cp++ = *msg;
+		if (*msg++ == '\n') {
+			strcpy(cp, "error+ ");
+			cp += sizeof("error+ ") - 1;
+		}
+	}
+	*cp = 0;
+	syslog(0, "mail", "%s", buf);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/main.c
@@ -1,0 +1,571 @@
+#include "common.h"
+#include "send.h"
+
+/* globals to all files */
+int	flagn;
+int	flagx;
+int	debug;
+int	flagi  = 1;
+int	rmail;
+int	nosummary;
+char	*thissys;
+char	*altthissys;
+
+/* 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	int	catchint(void*, char*);
+
+void
+usage(void)
+{
+	fprint(2, "usage: send [-#bdirx] list-of-addresses\n");
+	exits("usage");
+}
+
+void
+main(int argc, char *argv[])
+{
+	int rv;
+	dest *dp;
+
+	ARGBEGIN{
+	case '#':
+		flagn = 1;
+		break;
+	case 'b':
+		add822headers = 0;
+		break;
+	case 'd':
+		debug = 1;
+		break;
+	case 'i':
+		flagi = 0;
+		break;
+	case 'r':
+		rmail++;
+		break;
+	case 'x':
+		flagn = 1;
+		flagx = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	tmfmtinstall();
+	if(*argv == 0)
+		usage();
+	dp = 0;
+	for(; *argv; argv++){
+		if(shellchars(*argv)){
+			fprint(2, "illegal characters in destination\n");
+			exits("syntax");
+		}
+		d_insert(&dp, d_new(s_copy(*argv)));
+	}
+	arglist = d_to(dp);
+
+	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 (!flagn) {
+		Binit(&in, 0, OREAD);
+		if(!rmail)
+			atnotify(catchint, 1);
+		mp = m_read(&in, rmail, !flagi);
+		if (mp == 0)
+			exits(0);
+		if (interrupt != 0) {
+			save_mail(mp);
+			exits("interrupt");
+		}
+	} else {
+		mp = m_new();
+		if(default_from(mp) < 0){
+			fprint(2, "%s: can't determine login name\n", argv0);
+			exits("fail");
+		}
+	}
+	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)
+		exits(refuse(dp, mp, "possible forward loop", 0, 0)? "refuse": "");
+
+	/*
+	 *  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)
+		exits(refuse(dp, mp, "message too long", 0, 0)? "refuse": "");
+
+	rv = send(dp, mp, rmail);
+	if(savemail)
+		save_mail(mp);
+	if(mp)
+		m_free(mp);
+	exits(rv? "fail": "");
+}
+
+/* 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:
+			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;
+	if(rmail || flagn || forked)
+		return;
+	switch(fork()){
+	case -1:
+		break;
+	case 0:
+		sysdetach();
+		for(i=0; i<3; i++)
+			close(i);
+		savemail = 0;
+		forked = 1;
+		break;
+	default:
+		exits("");
+	}
+}
+
+
+/* save the mail */
+static void
+save_mail(message *mp)
+{
+	char buf[Pathlen];
+	Biobuf *fp;
+
+	mboxpathbuf(buf, sizeof buf, getlog(), "dead.letter");
+	fp = sysopen(buf, "cAt", 0660);
+	if (fp == 0)
+		return;
+	m_bprint(mp, fp);
+	sysclose(fp);
+	fprint(2, "saved in %s\n", buf);
+}
+
+/* remember the interrupt happened */
+
+static int
+catchint(void*, char *msg)
+{
+	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 (flagn) {
+		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)
+{
+	int status;
+	char *none;
+	dest *next, *list;
+	process *pp;
+	String *cmd;
+	String *errstring;
+
+	errstring = s_new();
+	list = 0;
+
+	/*
+	 * we're just protecting users from their own
+	 * pipeto scripts with this none business.
+	 * this depends on none being able to append
+	 * to a mail file.
+	 */
+
+	if (dp->status == d_pipeto)
+		none = "none";
+	else
+		none = 0;
+	/*
+	 *  collect the arguments
+	 */
+	next = d_rm_same(&dp);
+	if(flagx)
+		cmd = s_new();
+	else
+		cmd = s_clone(next->repl1);
+	for(; next != 0; next = d_rm_same(&dp)){
+		if(flagx){
+			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 (flagn) {
+		if(flagx)
+			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;
+}
+
+/*
+ *  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();
+	String *boundary;
+	dest *ndp;
+	char *rcvr, now[128];
+	int rv;
+	Tm tm;
+
+	boundary = mkboundary();
+
+	refp->bulk = 1;
+	refp->rfc822headers = 1;
+	snprint(now, sizeof(now), "%τ", thedate(&tm));
+	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, now);
+	refp->haveto = 1;
+	s_append(refp->body, "To: ");
+	s_append(refp->body, rcvr);
+	s_append(refp->body, "\n"
+		"Subject: bounced mail\n"
+		"MIME-Version: 1.0\n"
+		"Content-Type: multipart/mixed;\n"
+		"\tboundary=\"");
+	s_append(refp->body, s_to_c(boundary));
+	s_append(refp->body, "\"\n"
+		"Content-Disposition: inline\n"
+		"\n"
+		"This is a multi-part message in MIME format.\n"
+		"--");
+	s_append(refp->body, s_to_c(boundary));
+	s_append(refp->body, "\n"
+		"Content-Disposition: inline\n"
+		"Content-Type: text/plain; charset=\"US-ASCII\"\n"
+		"Content-Transfer-Encoding: 7bit\n"
+		"\n"
+		"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"
+		"Content-Type: message/rfc822\n"
+		"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;
+}
+
+static void
+appaddr(String *sp, dest *dp)
+{
+	dest *p;
+	String *s;
+
+	if (dp->parent != 0) {
+		for(p = dp->parent; p->parent; p = p->parent)
+			;
+		s = unescapespecial(s_clone(p->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);
+}
+
+/* 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);
+}
+
+/*
+ *  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)
+{
+	int rv;
+	String *errstring;
+	dest *dp;
+
+	errstring = s_new();
+	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));
+
+	rv = 1;
+	if(rmail){
+		/* accept it or request a retry */
+		if(outofresources){
+			fprint(2, "Mail %s\n", s_to_c(errstring));
+		} else {
+			/*
+			 *  reject without generating a reply, smtpd returns
+			 *  5.0.0 status when it sees "mail refused"
+			 */
+			fprint(2, "mail refused: %s\n",  s_to_c(errstring));
+		}
+	} 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;
+		}
+	}
+
+	s_free(errstring);
+	return rv;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/message.c
@@ -1,0 +1,555 @@
+#include "common.h"
+#include "send.h"
+#include <regexp.h>
+#include "../smtp/smtp.h"
+#include "../smtp/rfc822.tab.h"
+
+enum{
+	VMLIMIT	= 64*1024,
+	MSGLIMIT	= 128*1024*1024,
+};
+
+/* global to this file */
+static Reprog *rfprog;
+static Reprog *fprog;
+
+int received;	/* from rfc822.y */
+
+static String*	getstring(Node *p);
+static String*	getaddr(Node *p);
+
+int
+default_from(message *mp)
+{
+	char *cp, *lp, now[128];
+	Tm tm;
+
+	cp = getenv("upasname");
+	lp = getlog();
+	snprint(now, sizeof(now), "%τ", thedate(&tm));
+	if(lp == nil){
+		free(cp);
+		return -1;
+	}
+	if(cp && *cp)
+		s_append(mp->sender, cp);
+	else
+		s_append(mp->sender, lp);
+	free(cp);
+	s_append(mp->date, now);
+	return 0;
+}
+
+message*
+m_new(void)
+{
+	message *mp;
+
+	mp = (message*)mallocz(sizeof(message), 1);
+	if (mp == 0)
+		sysfatal("m_new: %r");
+	mp->sender = s_new();
+	mp->replyaddr = s_new();
+	mp->date = s_new();
+	mp->body = s_new();
+	mp->size = 0;
+	mp->fd = -1;
+	return mp;
+}
+
+void
+m_free(message *mp)
+{
+	if(mp->fd >= 0)
+		close(mp->fd);
+	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(mp);
+}
+
+/* read a message into a temp file, return an open fd to it in mp->fd */
+static int
+m_read_to_file(Biobuf *fp, message *mp)
+{
+	char buf[4*1024], file[Pathlen];
+	int fd, n;
+
+	snprint(file, sizeof file, "%s/mtXXXXXX", UPASTMP);
+	/*
+	 *  create temp file to be removed on close
+	 */
+	mktemp(file);
+	if((fd = create(file, ORDWR|ORCLOSE, 0600))<0)
+		return -1;
+
+	/*
+	 *  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));
+	return nil;
+}
+
+/* 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;
+}
+
+/* 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;
+}
+
+/* append a sub-expression match onto a String */
+static void
+append_match(Resub *subexp, String *sp, int se)
+{
+	char *cp, *ep;
+
+	cp = subexp[se].sp;
+	ep = subexp[se].ep;
+	for (; cp < ep; cp++)
+		s_putc(sp, *cp);
+	s_terminate(sp);
+}
+
+
+static char *REMFROMRE =
+	"^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)[ \t]+remote[ \t]+from[ \t]+(.*)\n$";
+static char *FROMRE =
+	"^>?From[ \t]+((\".*\")?[^\" \t]+?(\".*\")?[^\" \t]+?)[ \t]+(.+)\n$";
+
+enum{
+	REMSENDERMATCH 	= 1,
+	SENDERMATCH	= 1,
+	REMDATEMATCH 	= 4,
+	DATEMATCH		= 4,
+	REMSYSMATCH	= 5,
+};
+
+/* read in a message, interpret the 'From' header */
+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].sp!=subexp[REMSYSMATCH].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)
+			sysfatal("m_read: %r");
+		mp->size += n;
+		if(n == VMLIMIT)
+			if(m_read_to_file(fp, mp) < 0)
+				sysfatal("m_read: %r");
+	}
+
+	/*
+	 *  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' */
+int
+m_get(message *mp, vlong 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;
+	*pp = buf;
+	return pread(mp->fd, buf, sizeof buf, offset);
+}
+
+/* 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)
+{
+//	char *p;
+	int rv;
+	String *s;
+
+	if(!returnable(s_to_c(mp->sender)))
+		return Bprint(fp, "From: Postmaster\n");
+
+//	p = username(s_to_c(mp->sender));
+//	if(p) {
+//		s_append(s = s_new(), p);
+//		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 */
+int
+m_print(message *mp, Biobuf *fp, char *remote, int mbox)
+{
+	char *date, *d, *f[6];
+	int n, r;
+	String *sender;
+
+	sender = unescapespecial(s_clone(mp->sender));
+	date = s_to_c(mp->date);
+	if(remote)
+		r = Bprint(fp, "From %s %s remote from %s\n", s_to_c(sender), date, remote);
+	else
+		r = Bprint(fp, "From %s %s\n", s_to_c(sender), date);
+	s_free(sender);
+	if(r < 0)
+		return -1;
+	if(!rmail && !mp->havedate){
+		/* add a date: line Date: Sun, 19 Apr 1998 12:27:52 -0400 */
+		d = strdup(date);
+		n = getfields(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]));
+		free(d);
+	}
+	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 */
+int
+m_bprint(message *mp, Biobuf *fp)
+{
+	return m_noescape(mp, fp);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/mkfile
@@ -1,0 +1,45 @@
+</$objtype/mkfile
+
+TARG=\
+	send\
+	filter\
+
+LIB=../common/libcommon.a$O
+
+OFILES=\
+	message.$O\
+	dest.$O\
+	log.$O\
+	skipequiv.$O\
+	../smtp/rfc822.tab.$O\
+
+SOBJ=\
+	authorize.$O\
+	bind.$O\
+	cat_mail.$O\
+	gateway.$O\
+	local.$O\
+	main.$O\
+	rewrite.$O\
+	translate.$O\
+
+FOBJ=cat_mail.$O
+
+HFILES=\
+	send.h\
+	../common/common.h\
+	../common/sys.h\
+
+</sys/src/cmd/mkmany
+<../mkupas
+CFLAGS=$CFLAGS -I../common
+
+$O.send: $SOBJ $OFILES
+	$LD $LDFLAGS -o $target $prereq $LIB
+
+$O.filter: $FOBJ
+
+message.$O: ../smtp/rfc822.tab.h
+
+../smtp/rfc822.tab.h ../smtp/rfc822.tab.$O: ../smtp/rfc822.y
+	cd ../smtp && mk rfc822.tab.h rfc822.tab.$O
--- /dev/null
+++ b/sys/src/cmd/upas/send/regtest.c
@@ -1,0 +1,36 @@
+#include <u.h>
+#include <libc.h>
+#include <regexp.h>
+#include <bio.h>
+
+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> ");
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/rewrite.c
@@ -1,0 +1,313 @@
+#include "common.h"
+#include "send.h"
+#include <regexp.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.
+ */
+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)
+		sysfatal("malloc: %r");
+	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:
+ *	<reg exp> <String> <repl exp> [<repl exp>]
+ */
+int
+getrules(void)
+{
+	char file[Pathlen];
+	Biobuf *rfp;
+	String *line, *type;
+
+	snprint(file, sizeof file, "%s/rewrite", UPASLIB);
+	rfp = sysopen(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);
+	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)
+			fprint(2, "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].sp)
+		if((s_to_c(addrp) + strlen(s_to_c(addrp))) == rp->subexp[0].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
+ */
+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){
+		fprint(2, "\t->");
+		if(dp->repl1)
+			fprint(2, "%s", s_to_c(dp->repl1));
+		if(dp->repl2)
+			fprint(2, "%s", s_to_c(dp->repl2));
+		fprint(2, "\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].sp != 0)
+					for (s = subexp[i].sp;
+					     s < subexp[i].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].sp != 0)
+				for (s = subexp[0].sp;
+				     s < subexp[0].ep; s++)
+					s_putc(stp, *s);
+		} else
+			s_putc(stp, *sp);
+		sp++;
+	}
+	s_terminate(stp);
+
+	return s_restart(stp);
+}
+
+void
+regerror(char* s)
+{
+	fprint(2, "rewrite: %s\n", s);
+	/* make sure the message is seen locally */
+	syslog(0, "mail", "error in rewrite: %s", s);
+}
+
+//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:"...");
+//	}
+//}
--- /dev/null
+++ b/sys/src/cmd/upas/send/send.h
@@ -1,0 +1,100 @@
+#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	*to;
+	int	size;
+	int	fd;		/* if >= 0, the file the message is stored in*/
+	String	*havefrom;
+	String	*havesender;
+	String	*havereplyto;
+	char	havedate;
+	char	havemime;
+	String	*havesubject;
+	char	rfc822headers;
+	char	*boundary;	/* bondary marker for attachments */
+	char	haveto;
+	char	bulk;		/* if Precedence: Bulk in header */
+	char	received;	/* number of received lines */
+};
+
+extern	int	rmail;
+extern	int	onatty;
+extern	char	*thissys;
+extern	char	*altthissys;
+extern	int	debug;
+extern	int	nosummary;
+extern	int	flagn;
+extern	int	flagx;
+
+extern void	authorize(dest*);
+extern int	cat_mail(dest*, message*);
+extern dest	*up_bind(dest*, message*, int);
+extern int	ok_to_forward(char*);
+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*, vlong, 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);
--- /dev/null
+++ b/sys/src/cmd/upas/send/skipequiv.c
@@ -1,0 +1,74 @@
+#include "common.h"
+#include "send.h"
+
+#define isspace(c) ((c)==' ' || (c)=='\t' || (c)=='\n')
+
+static int
+okfile(char *s, Biobuf *b)
+{
+	char *buf, *p, *e;
+	int len, c;
+
+	len = strlen(s);
+	Bseek(b, 0, 0);
+	
+	/* one iteration per system name in the file */
+	while(buf = Brdline(b, '\n')) {
+		e = buf + Blinelen(b);
+		for(p = buf; p < e;){
+			while(isspace(*p) || *p==',')
+				p++;
+			if(strncmp(p, s, len) == 0) {
+				c = p[len];
+				if(isspace(c) || c==',')
+					return 1;
+			}
+			while(p < e && (!isspace(*p)) && *p!=',')
+				p++;
+		}
+	}
+	/* didn't find it, prohibit forwarding */
+	return 0;
+}
+
+/* return 1 if name found in file
+ *	  0 if name not found
+ *	  -1 if
+ */
+static int
+lookup(char *s, char *local, Biobuf **b)
+{
+	char file[Pathlen];
+
+	snprint(file, sizeof file, "%s/%s", UPASLIB, local);
+	if(*b != nil || (*b = sysopen(file, "r", 0)) != nil)
+		return okfile(s, *b);
+	return 0;
+}
+
+/*
+ *  skip past all systems in equivlist
+ */
+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)==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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/send/translate.c
@@ -1,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), 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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/greylist.c
@@ -1,0 +1,283 @@
+/*
+ * greylisting is the practice of making unknown callers call twice, with
+ * a pause between them, before accepting their mail and adding them to a
+ * whitelist of known callers.
+ *
+ * There's a bit of a problem with yahoo and other large sources of mail;
+ * they 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.  To cope
+ * better with this, we immediately accept mail from any system on the
+ * same class C subnet (IPv4 /24) as anybody on our whitelist, since the
+ * mail-sending machines tend to be clustered within a class C subnet.
+ *
+ * Various other goofballs, notably the IEEE, try to send mail just
+ * before 9 AM, then refuse to try again until after 5 PM. D'oh!
+ */
+#include "common.h"
+#include "smtpd.h"
+#include "smtp.h"
+#include <ctype.h>
+#include <ip.h>
+#include <ndb.h>
+
+enum {
+	Nonspammax = 14*60*60,  /* must call back within this time if real */
+	Nonspammin = 5*60,	/* must wait this long to retry */
+};
+
+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;
+
+static char whitelist[] = "/mail/grey/whitelist";
+
+/*
+ * matches ip addresses or subnets in whitelist against nci->rsys.
+ * ignores comments and blank lines in /mail/grey/whitelist.
+ */
+static int
+onwhitelist(void)
+{
+	char *line, *p;
+	Biobuf *wl;
+
+	wl = Bopen(whitelist, OREAD);
+	if (wl == nil)
+		return 0;
+	while ((line = Brdline(wl, '\n')) != nil) {
+		line[Blinelen(wl)-1] = '\0';		/* clobber newline */
+		p = strpbrk(line, " \t");
+		if (p)
+			*p = 0;
+		if (line[0] == '#' || line[0] == 0)
+			continue;
+		if(ipcheck(line))
+			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)
+{
+	int fd;
+	long mtime = -1;
+	Dir *ds;
+
+	fd = open(file, ORDWR);
+	if (fd < 0)
+		return mtime;
+	ds = dirfstat(fd);
+	if (ds != nil) {
+		mtime = ds->mtime;
+		/*
+		 * new twist: update file's mtime after reading it,
+		 * so each call resets the future time after which
+		 * we'll accept calls.  thus spammers who keep pounding
+		 * us lose, but just pausing for a few minutes and retrying
+		 * will succeed.
+		 */
+		if (0) {
+			/*
+			 * apparently none can't do this wstat
+			 * (permission denied);
+			 * more undocumented whacky none behaviour.
+			 */
+			ds->mtime = time(0);
+			if (dirfwstat(fd, ds) < 0)
+				syslog(0, "smtpd", "dirfwstat %s: %r", file);
+		}
+		free(ds);
+		write(fd, "x", 1);
+	}
+	close(fd);
+	return mtime;
+}
+
+static void
+tryaddgrey(char *file, Greysts *gsp)
+{
+	int fd = create(file, OWRITE|OEXCL, 0666);
+
+	gsp->created = (fd >= 0);
+	if (fd >= 0) {
+		close(fd);
+		gsp->existed = 0;  /* just created; couldn't have existed */
+		gsp->mtime = time(0);
+	} 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 >= Nonspammin && 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/tmp/%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 just minutes 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 */
+			fqdn = csgetvalue(nci->root, "ip", nci->rsys, "dom",
+				nil);
+			if (fqdn != nil)
+				fprint(fd, "%s %s\n", nci->rsys, fqdn);
+			else
+				fprint(fd, "%s\n", nci->rsys);
+			free(fqdn);
+			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");
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/mkfile
@@ -1,0 +1,42 @@
+</$objtype/mkfile
+
+TARG=\
+	smtpd\
+	smtp\
+
+LIB=../common/libcommon.a$O
+OFILES=
+HFILES=\
+	../common/common.h\
+	../common/sys.h\
+	smtpd.h\
+	smtp.h\
+	rfc822.tab.h\
+
+TEST=parsetest
+
+CLEANFILES=rfc822.tab.h rfc822.tab.c smtpd.tab.c
+
+</sys/src/cmd/mkmany
+<../mkupas
+CFLAGS=$CFLAGS -I../common
+
+$O.smtpd:\
+	smtpd.tab.$O\
+	spam.$O\
+	rfc822.tab.$O\
+	greylist.$O\
+
+$O.smtp: rfc822.tab.$O mxdial.$O
+
+smtpd.tab.c: smtpd.y
+	yacc -o xxx smtpd.y
+	sed 's/yy/zz/g' < xxx > $target
+	rm xxx
+
+rfc822.tab.c rfc822.tab.h:D: rfc822.y
+	yacc -d -s rfc822 rfc822.y
+
+$O.parsetest: rfc822.tab.$O
+
+parsetest.$O: rfc822.tab.$O
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/mxdial.c
@@ -1,0 +1,336 @@
+#include "common.h"
+#include "smtp.h"
+#include <ndb.h>
+
+char	*bustedmxs[Maxbustedmx];
+
+static void
+expand(DS *ds)
+{
+	char *s;
+	Ndbtuple *t;
+
+	s = ds->host + 1;
+	t = csipinfo(ds->netdir, "sys", sysname(), &s, 1);
+	if(t != nil){
+		strecpy(ds->expand, ds->expand+sizeof ds->expand, t->val);
+		ds->host = ds->expand;
+	}
+	ndbfree(t);
+}
+
+/* break up an address to its component parts */
+void
+dialstringparse(char *str, DS *ds)
+{
+	char *p, *p2;
+
+	strecpy(ds->buf, ds->buf + sizeof ds->buf, str);
+	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;
+	else
+		ds->service = "smtp";
+	if(*ds->host == '$')
+		expand(ds);
+}
+
+void
+mxtabfree(Mxtab *mx)
+{
+	free(mx->mx);
+	memset(mx, 0, sizeof *mx);
+}
+
+static void
+mxtabrealloc(Mxtab *mx)
+{
+	if(mx->nmx < mx->amx)
+		return;
+	if(mx->amx == 0)
+		mx->amx = 1;
+	mx->amx <<= 1;
+	mx->mx = realloc(mx->mx, sizeof mx->mx[0] * mx->amx);
+	if(mx->mx == nil)
+		sysfatal("no memory for mx");
+}
+
+static void
+mxtabadd(Mxtab *mx, char *host, char *ip, char *net, int pref)
+{
+	int i;
+	Mx *x;
+
+	mxtabrealloc(mx);
+	x = mx->mx;
+	for(i = mx->nmx; i>0 && x[i-1].pref>pref && x[i-1].netdir == net; i--)
+		x[i] = x[i-1];
+	strecpy(x[i].host, x[i].host + sizeof x[i].host, host);
+	if(ip != nil)
+		strecpy(x[i].ip, x[i].ip + sizeof x[i].ip, ip);
+	else
+		x[i].ip[0] = 0;
+	x[i].netdir = net;
+	x[i].pref = pref;
+	x[i].valid = 1;
+	mx->nmx++;
+}
+
+static int
+timeout(void*, char *msg)
+{
+	if(strstr(msg, "alarm"))
+		return 1;
+	return 0;
+}
+
+static long
+timedwrite(int fd, void *buf, long len, long ms)
+{
+	long n, oalarm;
+
+	atnotify(timeout, 1);
+	oalarm = alarm(ms);
+	n = pwrite(fd, buf, len, 0);
+	alarm(oalarm);
+	atnotify(timeout, 0);
+	return n;
+}
+
+static int
+dnslookup(Mxtab *mx, int fd, char *query, char *domain, char *net, int pref0)
+{
+	int n;
+	char buf[1024], *f[4];
+
+	n = timedwrite(fd, query, strlen(query), 60*1000);
+	if(n < 0){
+		rerrstr(buf, sizeof buf);
+		dprint("dns: %s\n", buf);
+		if(strstr(buf, "dns failure")){
+			/* if dns fails for the mx lookup, we have to stop */
+			close(fd);
+			return -1;
+		}
+		return 0;
+	}
+
+	seek(fd, 0, 0);
+	for(;;){
+		if((n = read(fd, buf, sizeof buf - 1)) < 1)
+			break;
+		buf[n] = 0;
+	//	chat("dns: %s\n", buf);
+		n = tokenize(buf, f, nelem(f));
+		if(n < 2)
+			continue;
+		if(strcmp(f[1], "mx") == 0 && n == 4){
+			if(strchr(domain, '.') == 0)
+				strcpy(domain, f[0]);
+			mxtabadd(mx, f[3], nil, net, atoi(f[2]));
+		}
+		else if (strcmp(f[1], "ip") == 0 && n == 3){
+			if(strchr(domain, '.') == 0)
+				strcpy(domain, f[0]);
+			mxtabadd(mx, f[0], f[2], net, pref0);
+		}
+	}
+
+	return 0;
+}
+
+static int
+busted(char *mx)
+{
+	char **bmp;
+
+	for (bmp = bustedmxs; *bmp != nil; bmp++)
+		if (strcmp(mx, *bmp) == 0)
+			return 1;
+	return 0;
+}
+
+static void
+complain(Mxtab *mx, char *domain)
+{
+	char buf[1024], *e, *p;
+	int i;
+
+	p = buf;
+	e = buf + sizeof buf;
+	for(i = 0; i < mx->nmx; i++)
+		p = seprint(p, e, "%s ", mx->mx[i].ip);
+	syslog(0, "smtpd.mx", "loopback for %s %s", domain, buf);
+}
+
+static int
+okaymx(Mxtab *mx, char *domain)
+{
+	int i;
+	Mx *x;
+
+	/* look for malicious dns entries; TODO use badcidr in ../spf/ to catch more than ip4 */
+	for(i = 0; i < mx->nmx; i++){
+		x = mx->mx + i;
+		if(x->valid && strcmp(x->ip, "127.0.0.1") == 0){
+			dprint("illegal: domain %s lists 127.0.0.1 as mail server", domain);
+			complain(mx, domain);
+			werrstr("illegal: domain %s lists 127.0.0.1 as mail server", domain);
+			return -1;
+		}
+		if(x->valid && busted(x->host)){
+			dprint("lookup: skipping busted mx %s\n", x->host);
+			x->valid = 0;
+		}
+	}
+	return 0;
+}
+
+static int
+lookup(Mxtab *mx, char *net, char *host, char *domain, char *type)
+{
+	char dns[128], buf[1024];
+	int fd, i;
+	Mx *x;
+
+	snprint(dns, sizeof dns, "%s/dns", net);
+	fd = open(dns, ORDWR);
+	if(fd == -1)
+		return -1;
+
+	snprint(buf, sizeof buf, "%s %s", host, type);
+	dprint("sending %s '%s'\n", dns, buf);
+	dnslookup(mx, fd, buf, domain, net, 10000);
+
+	for(i = 0; i < mx->nmx; i++){
+		x = mx->mx + i;
+		if(x->ip[0] != 0)
+			continue;
+		x->valid = 0;
+
+		snprint(buf, sizeof buf, "%s %s", x->host, "ip");
+		dprint("sending %s '%s'\n", dns, buf);
+		dnslookup(mx, fd, buf, domain, net, x->pref);
+	}
+
+	close(fd);
+
+	if(strcmp(type, "mx") == 0){
+		if(okaymx(mx, domain) == -1)
+			return -1;
+		for(i = 0; i < mx->nmx; i++){
+			x = mx->mx + i;
+			dprint("mx list: %s	%d	%s\n", x->host, x->pref, x->ip);
+		}
+		dprint("\n");
+	}
+
+	return 0;
+}
+
+static int
+lookcall(Mxtab *mx, DS *d, char *domain, char *type)
+{
+	char buf[1024];
+	int i;
+	Mx *x;
+
+	if(lookup(mx, d->netdir, d->host, domain, type) == -1){
+		for(i = 0; i < mx->nmx; i++)
+			if(mx->mx[i].netdir == d->netdir)
+				mx->mx[i].valid = 0;
+		return -1;
+	}
+
+	for(i = 0; i < mx->nmx; i++){
+		x = mx->mx + i;
+		if(x->ip[0] == 0 || x->valid == 0){
+			x->valid = 0;
+			continue;
+		}
+		snprint(buf, sizeof buf, "%s/%s!%s!%s", d->netdir, d->proto,
+			x->ip /*x->host*/, d->service);
+		dprint("mxdial trying %s	[%s]\n", x->host, buf);
+		atnotify(timeout, 1);
+		alarm(10*1000);
+		mx->fd = dial(buf, 0, 0, 0);
+		alarm(0);
+		atnotify(timeout, 0);
+		if(mx->fd >= 0){
+			mx->pmx = i;
+			return mx->fd;
+		}
+		dprint("	failed %r\n");
+		x->valid = 0;
+	}
+
+	return -1;
+}
+
+int
+mxdial0(char *addr, char *ddomain, char *gdomain, Mxtab *mx)
+{
+	int nd, i, j;
+	DS *d;
+	static char *tab[] = {"mx", "ip", };
+
+	dprint("mxdial(%s, %s, %s, mx)\n", addr, ddomain, gdomain);
+	memset(mx, 0, sizeof *mx);
+	d = mx->ds;
+	dialstringparse(addr, d + 0);
+	nd = 1;
+	if(d[0].netdir == nil){
+		d[1] = d[0];
+		d[0].netdir = "/net";
+		d[1].netdir = "/net.alt";
+		nd = 2;
+	}
+
+	/* search all networks for mx records; then ip records */
+	for(j = 0; j < nelem(tab); j++)
+		for(i = 0; i < nd; i++)
+			if(lookcall(mx, d + i, ddomain, tab[j]) != -1)
+				return mx->fd;
+
+	/* grotty: try gateway machine by ip only (fixme: try cs lookup) */
+	if(gdomain != nil){
+		dialstringparse(netmkaddr(gdomain, 0, "smtp"), d + 0);
+		if(lookcall(mx, d + 0, gdomain, "ip") != -1)
+			return mx->fd;
+	}
+
+	return -1;
+}
+
+int
+mxdial(char *addr, char *ddomain, char *gdomain, Mx *x)
+{
+	int fd;
+	Mxtab mx;
+
+	memset(x, 0, sizeof *x);
+	fd = mxdial0(addr, ddomain, gdomain, &mx);
+	if(fd >= 0 && mx.pmx >= 0)
+		*x = mx.mx[mx.pmx];
+	mxtabfree(&mx);
+	return fd;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/parsetest.c
@@ -1,0 +1,86 @@
+#include <u.h>
+#include <libc.h>
+#include <String.h>
+#include <bio.h>
+#include "smtp.h"
+
+Biobuf o;
+
+void
+freefields(void)
+{
+	Field *f, *fn;
+	Node *n, *nn;
+
+	for(f = firstfield; f != nil; f = fn){
+		fn = f->next;
+		for(n = f->node; n != nil; n = nn){
+			nn = n->next;
+			s_free(n->s);
+			s_free(n->white);
+			free(n);
+		}
+		free(f);
+	}
+	firstfield = nil;
+}
+
+void
+printhdr(void)
+{
+	Field *f;
+	Node *n;
+
+	for(f = firstfield; f != nil; f = f->next){
+		for(n = f->node; n != nil; n = n->next){
+			if(n->s != nil)
+				Bprint(&o, "%s", s_to_c(n->s));
+			else
+				Bprint(&o, "%c", n->c);
+			if(n->white != nil)
+				Bprint(&o, "%s", s_to_c(n->white));
+		}
+		Bprint(&o, "\n");
+	}
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: parsetest file ...\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, fd, nbuf;
+	char *buf;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+
+	if(Binit(&o, 1, OWRITE) == -1)
+		sysfatal("Binit: %r");
+	for(i = 0; i < argc; i++){
+		fd = open(argv[i], OREAD);
+		if(fd == -1)
+			sysfatal("open: %r");
+		buf = malloc(128*1024);
+		if(buf == nil)
+			sysfatal("malloc: %r");
+		nbuf = read(fd, buf, 128*1024);
+		if(nbuf == -1)
+			sysfatal("read: %r");
+		close(fd);
+		yyinit(buf, nbuf);
+		yyparse();
+		printhdr();
+		freefields();
+		free(buf);
+		Bflush(&o);
+	}
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/rfc822.y
@@ -1,0 +1,755 @@
+%{
+#include "common.h"
+#include "smtp.h"
+#include <ctype.h>
+
+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		: fieldlist '\n'
+			{ yydone = 1; }
+		;
+fieldlist		: field '\n'
+		| fieldlist field '\n'
+		;
+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.
+ *
+ */
+yylex(void)
+{
+	char *start;
+	int quoting, escaping, c, d;
+	String *t;
+	Keyword *kp;
+
+//	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*)
+{
+//	fprint(2, "parse err: %s\n", x);
+}
+
+/*
+ *  parse white space and comments
+ */
+String *
+yywhite(void)
+{
+	String *w;
+	int clevel, c, 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;
+}
+
+/*
+ *  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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtp.c
@@ -1,0 +1,1209 @@
+#include "common.h"
+#include "smtp.h"
+#include <ctype.h>
+#include <mp.h>
+#include <libsec.h>
+#include <auth.h>
+
+static	char*	connect(char*, Mx*);
+static	char*	wraptls(void);
+static	char*	dotls(char*);
+static	char*	doauth(char*);
+
+void	addhostdom(String*, char*);
+String*	bangtoat(char*);
+String*	convertheader(String*);
+int	dBprint(char*, ...);
+#pragma varargck argpos dBprint 1
+int	dBputc(int);
+char*	data(String*, Biobuf*, Mx*);
+char*	domainify(char*, char*);
+String*	fixrouteaddr(String*, Node*, Node*);
+char*	getcrnl(String*);
+int	getreply(void);
+char*	hello(char*, int);
+char*	mailfrom(char*);
+int	printdate(Node*);
+int	printheader(void);
+void	putcrnl(char*, int);
+void	quit(char*);
+char*	rcptto(char*);
+char	*rewritezone(char *);
+
+char	Retry[] = "Retry, Temporary Failure";
+char	Giveup[] = "Permanent Failure";
+
+String	*reply;		/* last reply */
+String	*toline;
+
+int	alarmscale;
+int	autistic;
+int	debug;		/* true if we're debugging */
+int	filter;
+int	insecure;
+int	last = 'n';	/* last character sent by putcrnl() */
+int	ping;
+int	quitting;	/* when error occurs in quit */
+int	tryauth;	/* Try to authenticate, if supported */
+int	trysecure;	/* Try to use TLS if the other side supports it */
+int	nocertcheck; /* ignore unrecognized certs. Still logged */
+
+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];
+Mx	*tmmx;		/* global for timeout */
+
+Biobuf	bin;
+Biobuf	bout;
+Biobuf	berr;
+Biobuf	bfile;
+
+int
+Dfmt(Fmt *fmt)
+{
+	Mx *mx;
+
+	mx = va_arg(fmt->args, Mx*);
+	if(mx == nil || mx->host[0] == 0)
+		return fmtstrcpy(fmt, "");
+	else
+		return fmtprint(fmt, "(%s:%s)", mx->host, mx->ip);
+}
+#pragma	varargck	type	"D"	Mx*
+
+char*
+deliverytype(void)
+{
+	if(ping)
+		return "ping";
+	return "delivery";
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: smtp [-aACdfipst] [-b busted-mx] [-g gw] [-h host] "
+		"[-u user] [.domain] net!host[!service] sender rcpt-list\n");
+	exits(Giveup);
+}
+
+int
+timeout(void *, char *msg)
+{
+	syslog(0, "smtp.fail", "%s interrupt: %s: %s %D", deliverytype(), farend,  msg, tmmx);
+	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")){
+		fprint(2, "smtp timeout: connection closed to %s\n", farend);
+		if(quitting){
+			syslog(0, "smtp.fail", "%s closed pipe to %s %D", deliverytype(), farend, tmmx);
+			_exits(quitrv);
+		}
+		/* call _exits() to prevent Bio from trying to flush closed pipe */
+		_exits(Retry);
+	}
+	return 0;
+}
+
+void
+removenewline(char *p)
+{
+	int n = strlen(p) - 1;
+
+	if(n < 0)
+		return;
+	if(p[n] == '\n')
+		p[n] = 0;
+}
+
+void
+main(int argc, char **argv)
+{
+	char *phase, *addr, *rv, *trv, *host, *domain;
+	char **errs, *p, *e, hellodomain[256], allrx[512];
+	int i, ok, rcvrs, bustedmx;
+	String *from, *fromm, *sender;
+	Mx mx;
+	Tm tm;
+
+	alarmscale = 60*1000;	/* minutes */
+	tmfmtinstall();
+	quotefmtinstall();
+	mailfmtinstall();		/* 2047 encoding */
+	fmtinstall('D', Dfmt);
+	fmtinstall('[', encodefmt);
+	fmtinstall('H', encodefmt);
+	errs = malloc(argc*sizeof(char*));
+	reply = s_new();
+	host = 0;
+	bustedmx = 0;
+	ARGBEGIN{
+	case 'a':
+		tryauth = 1;
+		if(trysecure == 0)
+			trysecure = 1;
+		break;
+	case 'A':	/* autistic: won't talk to us until we talk (Verizon) */
+		autistic = 1;
+		break;
+	case 'b':
+		if(bustedmx >= Maxbustedmx)
+			sysfatal("more than %d busted mxs given", Maxbustedmx);
+		bustedmxs[bustedmx++] = EARGF(usage());
+		break;
+	case 'd':
+		debug = 1;
+		break;
+	case 'f':
+		filter = 1;
+		break;
+	case 'g':
+		gdomain = EARGF(usage());
+		break;
+	case 'h':
+		host = EARGF(usage());
+		break;
+	case 'i':
+		insecure = 1;
+		break;
+	case 'p':
+		alarmscale = 10*1000;	/* tens of seconds */
+		ping = 1;
+		break;
+	case 's':
+		if(trysecure == 0)
+			trysecure = 1;
+		break;
+	case 't':
+		trysecure = 2;
+		break;
+	case 'u':
+		user = EARGF(usage());
+		break;
+	case 'C':
+		nocertcheck = 1;
+		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();
+	if(user == nil)
+		user = getuser();
+	strcpy(hostdomain, domainify(host, domain));
+	strcpy(hellodomain, domainify(sysname_read(), domain));
+
+	/*
+	 *  get destination address
+	 */
+	if(*argv == 0)
+		usage();
+	addr = *argv++; argc--;
+	farend = addr;
+	if((rv = strrchr(addr, '!')) && rv[1] == '['){
+		syslog(0, "smtp.fail", "%s to %s failed: illegal address",
+			deliverytype(), addr);
+		exits(Giveup);
+	}
+
+	/*
+	 *  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
+	 */
+	rcvrs = 0;
+	phase = "";
+	USED(phase);			/* just in case */
+	if(filter){
+		Binit(&bout, 1, OWRITE);
+		rv = data(from, &bfile, nil);
+		if(rv != 0){
+			phase = "filter";
+			goto error;
+		}
+		exits(0);
+	}
+
+	/* mxdial uses its own timeout handler */
+	if((rv = connect(addr, &mx)) != 0)
+		exits(rv);
+
+	tmmx = &mx;
+	/* 10 minutes to get through the initial handshake */
+	atnotify(timeout, 1);
+	alarm(10*alarmscale);
+	if((rv = hello(hellodomain, 0)) != 0){
+		phase = "hello";
+		goto error;
+	}
+	alarm(10*alarmscale);
+	if((rv = mailfrom(s_to_c(from))) != 0){
+		phase = "mailfrom";
+		goto error;
+	}
+
+	ok = 0;
+	/* if any rcvrs are ok, we try to send the message */
+	phase = "rcptto";
+	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 && rcvrs == 0)
+		phase = "rcptto; no addresses";
+	if(ok == 0 || rv == Retry)
+		goto error;
+
+	if(ping){
+		quit(0);
+		exits(0);
+	}
+
+	rv = data(from, &bfile, &mx);
+	if(rv != 0)
+		goto error;
+	quit(0);
+	if(rcvrs == ok)
+		exits(0);
+
+	/*
+	 *  here when some but not all rcvrs failed
+	 */
+	fprint(2, "%τ connect to %s: %D %s:\n", thedate(&tm), addr, &mx, phase);
+	for(i = 0; i < rcvrs; i++){
+		if(errs[i]){
+			syslog(0, "smtp.fail", "delivery to %s at %s %D %s, failed: %s",
+				argv[i], addr, &mx, phase, errs[i]);
+			fprint(2, "  mail to %s failed: %s", argv[i], errs[i]);
+		}
+	}
+	exits(Giveup);
+
+	/*
+	 *  here when all rcvrs failed
+	 */
+error:
+	alarm(0);
+	removenewline(s_to_c(reply));
+	if(rcvrs > 0){
+		p = allrx;
+		e = allrx + sizeof allrx;
+		seprint(p, e, "to ");
+		for(i = 0; i < rcvrs - 1; i++)
+			p = seprint(p, e, "%s,", argv[i]);
+		seprint(p, e, "%s ", argv[i]);
+	}
+	syslog(0, "smtp.fail", "%s %s at %s %D %s failed: %s",
+		deliverytype(), allrx, addr, &mx, phase, s_to_c(reply));
+	fprint(2, "%τ connect to %s %D %s:\n%s\n", thedate(&tm), addr, &mx, phase, s_to_c(reply));
+	if(!filter)
+		quit(rv);
+	exits(rv);
+}
+
+/*
+ *  connect to the remote host
+ */
+static char *
+connect(char* net, Mx *mx)
+{
+	char buf[ERRMAX];
+	int fd;
+
+	fd = mxdial(net, ddomain, gdomain, mx);
+
+	if(fd < 0){
+		rerrstr(buf, sizeof buf);
+		Bprint(&berr, "smtp: %s (%s) %D\n", buf, net, mx);
+		syslog(0, "smtp.fail", "%s %s (%s) %D", deliverytype(), buf, net, mx);
+		if(strstr(buf, "illegal")
+		|| strstr(buf, "unknown")
+		|| strstr(buf, "can't translate"))
+			return Giveup;
+		else
+			return Retry;
+	}
+	Binit(&bin, fd, OREAD);
+	fd = dup(fd, -1);
+	Binit(&bout, fd, OWRITE);
+	return 0;
+}
+
+static char smtpthumbs[] =	"/sys/lib/tls/smtp";
+static char smtpexclthumbs[] =	"/sys/lib/tls/smtp.exclude";
+
+static int
+tracetls(char *fmt, ...)
+{
+	va_list ap;
+	
+	va_start(ap, fmt);
+	Bvprint(&berr, fmt, ap);
+	Bprint(&berr, "\n");
+	Bflush(&berr);
+	va_end(ap);
+	return 0;
+}
+
+static char*
+wraptls(void)
+{
+	TLSconn *c;
+	Thumbprint *goodcerts;
+	char *err;
+	int fd;
+
+	goodcerts = nil;
+	err = Giveup;
+	c = mallocz(sizeof(*c), 1);
+	if (c == nil)
+		return err;
+
+	if (debug)
+		c->trace = tracetls;
+
+	fd = tlsClient(Bfildes(&bout), c);
+	if (fd < 0) {
+		syslog(0, "smtp", "tlsClient to %q: %r", ddomain);
+		goto Out;
+	}
+	Bterm(&bout);
+	Binit(&bout, fd, OWRITE);
+	fd = dup(fd, Bfildes(&bin));
+	Bterm(&bin);
+	Binit(&bin, fd, OREAD);
+
+	if (nocertcheck) {
+		syslog(0, "smtp", "ignoring cert for %s", ddomain);
+		err = nil;
+		goto Out;
+	}
+	goodcerts = initThumbprints(smtpthumbs, smtpexclthumbs, "x509");
+	if (goodcerts == nil) {
+		syslog(0, "smtp", "bad thumbprints in %s", smtpthumbs);
+		goto Out;
+	}
+	if (!okCertificate(c->cert, c->certlen, goodcerts)) {
+		syslog(0, "smtp", "cert for %s not recognized: %r", ddomain);
+		goto Out;
+	}
+	syslog(0, "smtp", "started TLS to %q", ddomain);
+	err = nil;
+Out:
+	freeThumbprints(goodcerts);
+	free(c->cert);
+	free(c->sessionID);
+	free(c);
+	return err;
+}
+
+/*
+ *  exchange names with remote host, attempt to
+ *  enable encryption and optionally authenticate.
+ *  not fatal if we can't.
+ */
+static char *
+dotls(char *me)
+{
+	char *err;
+
+	dBprint("STARTTLS\r\n");
+	if (getreply() != 2)
+		return Giveup;
+
+	err = wraptls();
+	if (err != nil)
+		return err;
+
+	return(hello(me, 1));
+}
+
+static char*
+smtpcram(DS *ds)
+{
+	char *p, ch[128], usr[64], rbuf[128], ubuf[128], ebuf[192];
+	int i, n, l;
+
+	dBprint("AUTH CRAM-MD5\r\n");
+	if(getreply() != 3)
+		return Retry;
+	p = s_to_c(reply) + 4;
+	l = dec64((uchar*)ch, sizeof ch, p, strlen(p));
+	ch[l] = 0;
+	n = auth_respond(ch, l, usr, sizeof usr, rbuf, sizeof rbuf, auth_getkey,
+		"proto=cram role=client server=%q user=%q",
+		ds->host, user);
+	if(n == -1){
+		if(temperror())
+			return Retry;
+		syslog(0, "smtp.fail", "failed to get challenge response: %r");
+		return Giveup;
+	}
+	if(usr[0] == 0)
+		return "cannot find user name";
+	for(i = 0; i < n; i++)
+		rbuf[i] = tolower(rbuf[i]);
+	l = snprint(ubuf, sizeof ubuf, "%s %.*s", usr, utfnlen(rbuf, n), rbuf);
+	snprint(ebuf, sizeof ebuf, "%.*[", l, ubuf);
+
+	dBprint("%s\r\n", ebuf);
+	if(getreply() != 2)
+		return Retry;
+	return nil;
+}
+
+static char *
+doauth(char *methods)
+{
+	char buf[1024], *err;
+	UserPasswd *p;
+	DS ds;
+	int n;
+
+	dialstringparse(farend, &ds);
+	if(strstr(methods, "CRAM-MD5"))
+		return smtpcram(&ds);
+	p = auth_getuserpasswd(nil,
+		"proto=pass service=smtp server=%q user=%q",
+		ds.host, user);
+	if (p == nil) {
+		if(temperror())
+			return Retry;
+		syslog(0, "smtp.fail", "failed to get userpasswd: %r");
+		return Giveup;
+	}
+	err = Retry;
+	if (strstr(methods, "LOGIN")){
+		dBprint("AUTH LOGIN\r\n");
+		if (getreply() != 3)
+			goto out;
+
+		dBprint("%.*[\r\n", (int)strlen(p->user), p->user);
+		if (getreply() != 3)
+			goto out;
+
+		dBprint("%.*[\r\n", (int)strlen(p->passwd), p->passwd);
+		if (getreply() != 2)
+			goto out;
+
+		err = nil;
+	}
+	else if (strstr(methods, "PLAIN")){
+		n = snprint(buf, sizeof(buf), "%c%s%c%s", 0, p->user, 0, p->passwd);
+		dBprint("AUTH PLAIN %.*[\r\n", n, buf);
+		memset(buf, 0, sizeof(buf));
+		if (getreply() != 2)
+			goto out;
+		err = nil;
+	} else
+		err = "No supported AUTH method";
+out:
+	memset(p->user, 0, strlen(p->user));
+	memset(p->passwd, 0, strlen(p->passwd));
+	free(p);
+	return err;
+}
+
+char*
+hello(char *me, int encrypted)
+{
+	char *ret, *s, *t;
+	int ehlo;
+	String *r;
+
+	if(!encrypted){
+		if(trysecure > 1){
+			if((ret = wraptls()) != nil)
+				return ret;
+			encrypted = 1;
+		}
+
+		/*
+		 * Verizon fails to print the smtp greeting banner when it
+		 * answers a call.  Send a no-op in the hope of making it
+		 * talk.
+		 */
+		if(autistic){
+			dBprint("NOOP\r\n");
+			getreply();	/* consume the smtp greeting */
+			/* next reply will be response to noop */
+		}
+		switch(getreply()){
+		case 2:
+			break;
+		case 5:
+			return Giveup;
+		default:
+			return Retry;
+		}
+	}
+
+	ehlo = 1;
+  Again:
+	if(ehlo)
+		dBprint("EHLO %s\r\n", me);
+	else
+		dBprint("HELO %s\r\n", me);
+	switch(getreply()){
+	case 2:
+		break;
+	case 5:
+		if(ehlo){
+			ehlo = 0;
+			goto Again;
+		}
+		return Giveup;
+	default:
+		return Retry;
+	}
+	r = s_clone(reply);
+	if(r == nil)
+		return Retry;	/* Out of memory or couldn't get string */
+
+	/* Invariant: every line has a newline, a result of getcrlf() */
+	for(s = s_to_c(r); (t = strchr(s, '\n')) != nil; s = t + 1){
+		*t = '\0';
+		if(!encrypted && trysecure &&
+		    (cistrcmp(s, "250-STARTTLS") == 0 ||
+		     cistrcmp(s, "250 STARTTLS") == 0)){
+			s_free(r);
+			return dotls(me);
+		}
+		if(tryauth && (encrypted || insecure) &&
+		    (cistrncmp(s, "250 AUTH", strlen("250 AUTH")) == 0 ||
+		     cistrncmp(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:
+		return 0;
+	case 5:
+		return Giveup;
+	default:
+		return Retry;
+	}
+}
+
+/*
+ *  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;
+}
+
+/*
+ *  send the damn thing
+ */
+char *
+data(String *from, Biobuf *b, Mx *mx)
+{
+	char *buf, *cp, errmsg[ERRMAX];
+	int n, nbytes, bufsize, eof;
+	String *fromline;
+
+	/*
+	 *  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;
+
+	if(messageid == 0){
+		uchar id[16];
+
+		genrandom(id, sizeof(id));
+		nbytes += dBprint("Message-ID: <%.*H@%s>\r\n",
+			sizeof(id), id, hostdomain);
+	}
+
+	if(originator == 0)
+		nbytes += dBprint("From: %s\r\n", s_to_c(fromline));
+	s_free(fromline);
+
+	if(destination == 0 && toline){
+		if(*s_to_c(toline) == '@')	/* route addr */
+			nbytes += dBprint("To: <%s>\r\n", s_to_c(toline));
+		else
+			nbytes += dBprint("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 %D", s_to_c(from),
+				nbytes, s_to_c(toline), mx);
+	}
+	return 0;
+}
+
+/*
+ *  we're leaving
+ */
+void
+quit(char *rv)
+{
+		/* 60 minutes to quit */
+	quitting = 1;
+	quitrv = rv;
+	alarm(60*alarmscale);
+	dBprint("QUIT\r\n");
+	getreply();
+	Bterm(&bout);
+	Bterm(&bfile);
+}
+
+/*
+ *  read a reply into a string, return the reply code
+ */
+int
+getreply(void)
+{
+	char *line;
+	int rv;
+
+	reply = s_reset(reply);
+	for(;;){
+		line = getcrnl(reply);
+		if(debug)
+			Bflush(&berr);
+		if(line == 0)
+			return -1;
+		if(!isdigit(line[0]) || !isdigit(line[1]) || !isdigit(line[2]))
+			return -1;
+		if(line[3] != '-')
+			break;
+	}
+	if(debug)
+		Bflush(&berr);
+	rv = atoi(line)/100;
+	return rv;
+}
+void
+addhostdom(String *buf, char *host)
+{
+	s_append(buf, "@");
+	s_append(buf, host);
+}
+
+/*
+ *	Convert from `bang' to `source routing' format.
+ *
+ *	   a.x.y!b.p.o!c!d ->	@a.x.y:[email protected]
+ */
+String *
+bangtoat(char *addr)
+{
+	char *field[128];
+	int i, j, d;
+	String *buf;
+
+	/* parse the '!' format address */
+	buf = s_new();
+	for(i = 0; addr; i++){
+		field[i] = addr;
+		addr = strchr(addr, '!');
+		if(addr)
+			*addr++ = 0;
+	}
+	if(i == 1){
+		s_append(buf, field[0]);
+		return buf;
+	}
+
+	/*
+	 *  count leading domain fields (non-domains don't count)
+	 */
+	for(d = 0; d < i - 1; d++)
+		if(strchr(field[d], '.') == 0)
+			break;
+	/*
+	 *  if there are more than 1 leading domain elements,
+	 *  put them in as source routing
+	 */
+	if(d > 1){
+		addhostdom(buf, field[0]);
+		for(j = 1; j< d - 1; j++){
+			s_append(buf, ",");
+			s_append(buf, "@");
+			s_append(buf, field[j]);
+		}
+		s_append(buf, ":");
+	}
+
+	/*
+	 *  throw in the non-domain elements separated by '!'s
+	 */
+	s_append(buf, field[d]);
+	for(j = d + 1; j <= i - 1; j++){
+		s_append(buf, "!");
+		s_append(buf, field[j]);
+	}
+	if(d)
+		addhostdom(buf, field[d-1]);
+	return buf;
+}
+
+/*
+ *  convert header addresses to @ format.
+ *  if the address is a source address, and a domain is specified,
+ *  make sure it falls in the domain.
+ */
+String*
+convertheader(String *from)
+{
+	char *s, buf[64];
+	Field *f;
+	Node *p, *lastp;
+	String *a;
+
+	if(!returnable(s_to_c(from))){
+		from = s_new();
+		s_append(from, "Postmaster");
+		addhostdom(from, hostdomain);
+	} else
+	if(strchr(s_to_c(from), '@') == 0){
+		if(s = username(s_to_c(from))){
+			/* this has always been here, but username() was broken */
+			snprint(buf, sizeof buf, "%U", s);
+			s_append(a = s_new(), buf);
+			s_append(a, " <");
+			s_append(a, s_to_c(from));
+			addhostdom(a, hostdomain);
+			s_append(a, ">");
+			from = a;
+		} else {
+			from = s_copy(s_to_c(from));
+			addhostdom(from, hostdomain);
+		}
+	} else
+		from = s_copy(s_to_c(from));
+	for(f = firstfield; f; f = f->next){
+		lastp = 0;
+		for(p = f->node; p; lastp = p, p = p->next){
+			if(!p->addr)
+				continue;
+			a = bangtoat(s_to_c(p->s));
+			s_free(p->s);
+			if(strchr(s_to_c(a), '@') == 0)
+				addhostdom(a, hostdomain);
+			else if(*s_to_c(a) == '@')
+				a = fixrouteaddr(a, p->next, lastp);
+			p->s = a;
+		}
+	}
+	return from;
+}
+/*
+ *	ensure route addr has brackets around it
+ */
+String*
+fixrouteaddr(String *raddr, Node *next, Node *last)
+{
+	String *a;
+
+	if(last && last->c == '<' && next && next->c == '>')
+		return raddr;			/* properly formed already */
+
+	a = s_new();
+	s_append(a, "<");
+	s_append(a, s_to_c(raddr));
+	s_append(a, ">");
+	s_free(raddr);
+	return a;
+}
+
+/*
+ *  print out the parsed header
+ */
+int
+printheader(void)
+{
+	char *cp, c[1];
+	int n, len;
+	Field *f;
+	Node *p;
+
+	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)
+{
+	char *p;
+	static String *s;
+
+	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, count;
+
+	count = 0;
+	for(;;){
+		c = Bgetc(&bin);
+		if(debug)
+			Bputc(&berr, c);
+		switch(c){
+		case -1:
+			s_append(s, "connection closed unexpectedly by remote system");
+			s_terminate(s);
+			return 0;
+		case '\r':
+			c = Bgetc(&bin);
+			if(c == '\n'){
+		case '\n':
+				s_putc(s, c);
+				if(debug)
+					Bputc(&berr, c);
+				count++;
+				s_terminate(s);
+				return s->ptr - count;
+			}
+			Bungetc(&bin);
+			s_putc(s, '\r');
+			if(debug)
+				Bputc(&berr, '\r');
+			count++;
+			break;
+		default:
+			s_putc(s, c);
+			count++;
+			break;
+		}
+	}
+}
+
+/*
+ *  print out a parsed date
+ */
+int
+printdate(Node *p)
+{
+	int n, sep;
+
+	n = dBprint("Date: %s,", s_to_c(p->s));
+	sep = 0;
+	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)
+{
+	char s;
+	int mindiff;
+	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
+ */
+
+int
+dBprint(char *fmt, ...)
+{
+	char buf[4096], *out;
+	int n;
+	va_list arg;
+
+	va_start(arg, fmt);
+	out = vseprint(buf, buf + sizeof buf, fmt, arg);
+	va_end(arg);
+	if(debug){
+		Bwrite(&berr, buf, out - buf);
+		Bflush(&berr);
+	}
+	n = Bwrite(&bout, buf,out - buf);
+	Bflush(&bout);
+	return n;
+}
+
+int
+dBputc(int x)
+{
+	if(debug)
+		Bputc(&berr, x);
+	return Bputc(&bout, x);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtp.h
@@ -1,0 +1,92 @@
+enum {
+	Maxbustedmx = 100,
+};
+
+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;
+typedef struct Mx	Mx;
+typedef struct Mxtab	Mxtab;
+
+struct DS {
+	/* dial string */
+	char	buf[128];
+	char	expand[128];
+	char	*netdir;
+	char	*proto;
+	char	*host;
+	char	*service;
+};
+
+struct Mx
+{
+	char	*netdir;
+	char	host[256];
+	char	ip[24];
+	int	pref;
+	int	valid;
+};
+
+struct Mxtab
+{
+	DS	ds[2];
+	int	nmx;
+	int	amx;
+	int	pmx;
+	int	fd;
+	Mx	*mx;
+};
+
+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	debug;
+extern int	messageid;
+extern char	*bustedmxs[Maxbustedmx];
+
+Node*	anonymous(Node*);
+Node*	address(Node*);
+int	badfieldname(Node*);
+Node*	bang(Node*, Node*);
+Node*	colon(Node*, Node*);
+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	mxdial0(char*, char*, char*, Mxtab*);
+int	mxdial(char*, char*, char*, Mx*);
+void	mxtabfree(Mxtab*);
+void	dialstringparse(char*, DS*);
+
+#define dprint(...)	do if(debug)print(__VA_ARGS__); while(0)
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtpd.c
@@ -1,0 +1,1794 @@
+#include "common.h"
+#include "smtpd.h"
+#include "smtp.h"
+#include <ctype.h>
+#include <ip.h>
+#include <ndb.h>
+#include <mp.h>
+#include <libsec.h>
+#include <auth.h>
+#include "rfc822.tab.h"
+
+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	Eflag;
+int	eflag;
+int	fflag;
+int	gflag;
+int	qflag;
+int	rflag;
+int	sflag;
+int	authenticate;
+int	authenticated;
+int	passwordinclear;
+char	*tlscert;
+
+uchar	rsysip[IPaddrlen];
+
+List	senders;
+List	rcvers;
+
+char	pipbuf[ERRMAX];
+char	*piperror;
+
+String*	mailerpath(char*);
+int	pipemsg(int*);
+int	rejectcheck(void);
+String*	startcmd(void);
+
+static int
+catchalarm(void*, char *msg)
+{
+	int ign;
+
+	ign = strstr(msg, "closed pipe") != nil;
+	if(ign)
+		return 0;
+	if(pp)
+		syskill(pp->pid);
+	return strstr(msg, "alarm") != nil;
+}
+
+/* override string error functions to do something reasonable */
+void
+s_error(char *f, char *status)
+{
+	char errbuf[ERRMAX];
+
+	errbuf[0] = 0;
+	rerrstr(errbuf, sizeof(errbuf));
+	if(f && *f)
+		reply("452 4.3.0 out of memory %s: %s\r\n", f, errbuf);
+	else
+		reply("452 4.3.0 out of memory %s\r\n", errbuf);
+	syslog(0, "smtpd", "++Malloc failure %s [%s]", him, nci->rsys);
+	exits(status);
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: smtpd [-DEadefghpqrs] [-c cert] [-k ip] [-m mailer] [-n net]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *netdir;
+	char buf[1024];
+	Tm tm;
+
+	netdir = nil;
+	tmfmtinstall();
+	quotefmtinstall();
+	fmtinstall('I', eipfmt);
+	fmtinstall('[', encodefmt);
+	ARGBEGIN{
+	case 'a':
+		authenticate = 1;
+		break;
+	case 'c':
+		tlscert = EARGF(usage());
+		break;
+	case 'D':
+		Dflag++;
+		break;
+	case 'd':
+		debug++;
+		break;
+	case 'E':
+		Eflag = 1;
+		break;			/* if you fail extra helo checks, you must authenticate */
+	case 'e':
+		eflag = 1;		/* disable extra helo checks */
+		break;
+	case 'f':				/* disallow relaying */
+		fflag = 1;
+		break;
+	case 'g':
+		gflag = 1;
+		break;
+	case 'h':				/* default domain name */
+		dom = EARGF(usage());
+		break;
+	case 'k':				/* prohibited ip address */
+		addbadguy(EARGF(usage()));
+		break;
+	case 'm':				/* set mail command */
+		mailer = mailerpath(EARGF(usage()));
+		break;
+	case 'n':				/* log peer ip address */
+		netdir = EARGF(usage());
+		break;
+	case 'p':
+		passwordinclear = 1;
+		break;
+	case 'q':
+		qflag = 1;		/* don't log invalid hello */
+		break;
+	case 'r':
+		rflag = 1;			/* verify sender's domain */
+		break;
+	case 's':				/* save blocked messages */
+		sflag = 1;
+		break;
+	default:
+		usage();
+	}ARGEND;
+
+	nci = getnetconninfo(netdir, 0);
+	if(nci == nil)
+		sysfatal("can't get remote system's address");
+	parseip(rsysip, nci->rsys);
+
+	if(mailer == nil)
+		mailer = mailerpath("send");
+
+	if(debug){
+		close(2);
+		snprint(buf, sizeof(buf), "%s/smtpd.db", UPASLOG);
+		if (open(buf, OWRITE) >= 0) {
+			seek(2, 0, 2);
+			fprint(2, "%d smtpd %τ\n", getpid(), thedate(&tm));
+		} else
+			debug = 0;
+	}
+	getconf();
+	if(isbadguy())
+		exits("");
+	Binit(&bin, 0, OREAD);
+
+	if (chdir(UPASLOG) < 0)
+		syslog(0, "smtpd", "no %s: %r", 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, *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 *lp);
+	lp->p = path;
+	lp->next = 0;
+
+	if(l->last)
+		l->last->next = lp;
+	else
+		l->first = lp;
+	l->last = lp;
+}
+
+int
+reply(char *fmt, ...)
+{
+	char buf[4096], *out;
+	int n;
+	va_list arg;
+
+	va_start(arg, fmt);
+	out = vseprint(buf, buf + 4096, fmt, arg);
+	va_end(arg);
+
+	n = out - buf;
+	if(debug) {
+		seek(2, 0, 2);
+		write(2, buf, n);
+	}
+	write(1, buf, n);
+	return n;
+}
+
+int
+ipcheck(char *s)
+{
+	uchar ip[IPaddrlen], mask[IPaddrlen];
+	uchar net[IPaddrlen], rnet[IPaddrlen];
+
+	if(parseipandmask(ip, mask, s, strchr(s, '/')) == -1)
+		return 0;
+	maskip(ip, mask, net);
+	maskip(rsysip, mask, rnet);
+	return ipcmp(net, rnet) == 0;
+}
+
+void
+reset(void)
+{
+	if(rejectcheck())
+		return;
+	listfree(&rcvers);
+	listfree(&senders);
+	if(filterstate != DIALUP){
+		logged = 0;
+		filterstate = ACCEPT;
+	}
+	reply("250 2.0.0 ok\r\n");
+}
+
+void
+sayhi(void)
+{
+	reply("220 %s ESMTP\r\n", dom);
+}
+
+Ndbtuple*
+rquery(char *d)
+{
+	Ndbtuple *t, *p;
+
+	t = dnsquery(nci->root, nci->rsys, "ptr");
+	for(p = t; p != nil; p = p->entry)
+		if(strcmp(p->attr, "dom") == 0
+		&& strcmp(p->val, d) == 0){
+			syslog(0, "smtpd", "ptr only from %s as %s",
+				nci->rsys, d);
+			return t;
+		}
+	ndbfree(t);
+	return nil;
+}
+
+int
+dnsexists(char *d)
+{
+	int r;
+	Ndbtuple *t;
+
+	r = -1;
+	if((t = dnsquery(nci->root, d, "any")) != nil || (t = rquery(d)) != nil)
+		r = 0;
+	ndbfree(t);
+	return r;
+}
+
+/*
+ * make callers from class A networks infested by spammers
+ * wait longer.
+ */
+
+static char netaspam[256] = {
+	[58]	1,
+	[66]	1,
+	[71]	1,
+
+	[76]	1,
+	[77]	1,
+	[78]	1,
+	[79]	1,
+	[80]	1,
+	[81]	1,
+	[82]	1,
+	[83]	1,
+	[84]	1,
+	[85]	1,
+	[86]	1,
+	[87]	1,
+	[88]	1,
+	[89]	1,
+
+	[190]	1,
+	[201]	1,
+	[217]	1,
+};
+
+static int
+delaysecs(void)
+{
+	if (netaspam[rsysip[0]])
+		return 60;
+	return 15;
+}
+
+static char *badtld[] = {
+	"localdomain",
+	"localhost",
+	"local",
+	"example",
+	"invalid",
+	"lan",
+	"test",
+};
+
+static char *bad2ld[] = {
+	"example.com",
+	"example.net",
+	"example.org"
+};
+
+int
+badname(void)
+{
+	char *p;
+
+	/*
+	 * similarly, if the claimed domain is not an address-literal,
+	 * require at least one letter, which there will be in
+	 * at least the last component (e.g., .com, .net) if it's real.
+	 * this rejects non-address-literal IP addresses,
+	 * among other bogosities.
+	 */
+	for (p = him; *p; p++)
+		if(isascii(*p) && isalpha(*p))
+			return 0;
+	return -1;
+}
+
+int
+ckhello(void)
+{
+	char *ldot, *rdot;
+	int i;
+
+	/*
+	 * it is 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 client software gets this wrong, so let trusted
+	 * (local) clients omit the dot.
+	 */
+	rdot = strrchr(him, '.');
+	if(rdot && rdot[1] == '\0') {
+		*rdot = '\0';			/* clobber trailing dot */
+		rdot = strrchr(him, '.');	/* try again */
+	}
+	if(rdot == nil)
+		return -1;
+	/*
+	 * Reject obviously bogus domains and those reserved by RFC 2606.
+	 */
+	if(rdot == nil)
+		rdot = him;
+	else
+		rdot++;
+	for(i = 0; i < nelem(badtld); i++)
+		if(!cistrcmp(rdot, badtld[i]))
+			return -1;
+	/* check second-level RFC 2606 domains: example\.(com|net|org) */
+	if(rdot != him)
+		*--rdot = '\0';
+	ldot = strrchr(him, '.');
+	if(rdot != him)
+		*rdot = '.';
+	if(ldot == nil)
+		ldot = him;
+	else
+		ldot++;
+	for(i = 0; i < nelem(bad2ld); i++)
+		if(!cistrcmp(ldot, bad2ld[i]))
+			return -1;
+	if(badname() == -1)
+		return -1;
+	if(dnsexists(him) == -1)
+		return -1;
+	return 0;
+}
+
+int
+heloclaims(void)
+{
+	char **s;
+
+	/*
+	 * 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(nci->rsys, nci->lsys) == 0)
+		return 0;
+	if(strcmp(him, dom) == 0)
+		return -1;
+	for(s = sysnames_read(); s && *s; s++)
+		if(cistrcmp(*s, him) == 0)
+			return -1;
+	if(him[0] != '[' && badname() == -1)
+		return -1;
+
+	return 0;
+}
+
+void
+hello(String *himp, int extended)
+{
+	int ck;
+
+	him = s_to_c(himp);
+	if(!qflag)
+		syslog(0, "smtpd", "%s from %s as %s", extended? "ehlo": "helo",
+			nci->rsys, him);
+	if(rejectcheck())
+		return;
+
+	ck = -1;
+	if(!trusted && nci)
+	if(heloclaims() || (!eflag && (ck = ckhello())))
+	if(ck && Eflag){
+		reply("250-you lie.  authentication required.\r\n");
+		authenticate = 1;
+	}else{
+		if(Dflag)
+			sleep(delaysecs()*1000);
+		if(!qflag)
+			syslog(0, "smtpd", "Hung up on %s; claimed to be %s",
+				nci->rsys, him);
+		rejectcount++;
+		reply("554 5.7.0 Liar!\r\n");
+		exits("client pretended to be us");
+		return;
+	}
+
+	if(strchr(him, '.') == 0 && nci != nil && strchr(nci->rsys, '.') != nil)
+		him = nci->rsys;
+
+	if(qflag)
+		syslog(0, "smtpd", "%s from %s as %s", extended? "ehlo": "helo",
+			nci->rsys, him);
+	if(Dflag)
+		sleep(delaysecs()*1000);
+	reply("250%c%s you are %s\r\n", extended ? '-' : ' ', dom, him);
+	if (extended) {
+		reply("250-ENHANCEDSTATUSCODES\r\n");	/* RFCs 2034 and 3463 */
+		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;
+
+	if(rejectcheck())
+		return;
+	if (authenticate && !authenticated) {
+		rejectcount++;
+		reply("530 5.7.0 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("501 5.1.3 Bad character in sender address %s.\r\n",
+			s_to_c(path));
+		return;
+	}
+
+	/*
+	 * see if this ip address, domain name, user name or account is blocked
+	 */
+	filterstate = blocked(path);
+
+	logged = 0;
+	listadd(&senders, path);
+	reply("250 2.0.0 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 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;
+
+	/*
+	 * 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 exploiting 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 && ipcmp(rsysip, dnsip) == 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 && ipcmp(rsysip, dnsip) == 0)
+				matched = 1;
+			next = nt->line;
+			free(nt);
+			nt = next;
+		} while (nt != first);
+	}
+	if (matched)
+		return 1;
+	else
+		return !mentioned;
+}
+
+void
+receiver(String *path)
+{
+	char *sender, *rcpt;
+
+	if(rejectcheck())
+		return;
+	if(him == 0 || *him == 0){
+		rejectcount++;
+		reply("503 Start by saying HELO, please\r\n");
+		return;
+	}
+	if(senders.last)
+		sender = s_to_c(senders.last->p);
+	else
+		sender = "<unknown>";
+
+	if(!recipok(s_to_c(path))){
+		rejectcount++;
+		syslog(0, "smtpd",
+		 "Disallowed %s (%s/%s) to blocked name %s",
+			sender, him, nci->rsys, s_to_c(path));
+		reply("550 5.1.1 %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 5.7.1 %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)) {
+		rejectcount++;
+		syslog(0, "smtpd", "Bad Forward %s (%s/%s) (%s)",
+			sender, him, nci->rsys, rcpt);
+		reply("550 5.7.1 we don't relay.  send to your-path@[] for "
+			"loopback.\r\n");
+		return;
+	}
+	listadd(&rcvers, path);
+	reply("250 2.0.0 receiver is %s\r\n", s_to_c(path));
+}
+
+void
+quit(void)
+{
+	reply("221 2.0.0 Successful termination\r\n");
+	close(0);
+	exits(0);
+}
+
+void
+noop(void)
+{
+	if(rejectcheck())
+		return;
+	reply("250 2.0.0 Nothing to see here. Move along ...\r\n");
+}
+
+void
+help(String *cmd)
+{
+	if(rejectcheck())
+		return;
+	if(cmd)
+		s_free(cmd);
+	reply("250 2.0.0 See http://www.ietf.org/rfc/rfc2821\r\n");
+}
+
+void
+verify(String *path)
+{
+	char *p, *q;
+	char *av[4];
+	static uint nverify;
+
+	if(rejectcheck())
+		return;
+	if(nverify++ >= 2)
+		sleep(1000 * (4 << nverify - 2));
+	if(shellchars(s_to_c(path))){
+		rejectcount++;
+		reply("503 5.1.3 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, 0, outstream(), 0, 1, 0);
+	if (pp == 0) {
+		reply("450 4.3.2 We're busy right now, try later\r\n");
+		return;
+	}
+
+	p = Brdline(pp->std[1]->fp, '\n');
+	if(p == 0){
+		reply("550 5.1.0 String does not match anything.\r\n");
+	} else {
+		p[Blinelen(pp->std[1]->fp) - 1] = 0;
+		if(strchr(p, ':'))
+			reply("550 5.1.0  String does not match anything.\r\n");
+		else{
+			q = strrchr(p, '!');
+			if(q)
+				p = q + 1;
+			reply("250 2.0.0 %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 0:
+			/* idiot html email! */
+			break;
+		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;
+	char *filename;
+	char **av;
+	Link *l;
+	String *cmd;
+
+	/*
+	 *  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 5.7.1 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-5.7.1 We don't accept mail from %s.\r\n",
+			s_to_c(senders.last->p));
+		reply("554 5.7.1 Contact postmaster@%s for more information.\r\n",
+			dom);
+		return 0;
+	case REFUSED:
+		logmsg("Refused");
+		rejectcount++;
+		reply("554 5.7.1 Sender domain must exist: %s\r\n",
+			s_to_c(senders.last->p));
+		return 0;
+	default:
+	case NONE:
+		logmsg("Confused");
+		rejectcount++;
+		reply("554-5.7.0 We have had an internal mailer error "
+			"classifying your message.\r\n");
+		reply("554-5.7.0 Filterstate is %d\r\n", filterstate);
+		reply("554 5.7.0 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 4.3.2 We're busy right now, try later\r\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 4.3.2 We're busy right now, try later\r\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, int *nbytes)
+{
+	int n, m;
+
+	if(p->s){
+		if(p->addr && strchr(s_to_c(p->s), '@') == nil)
+			n = Bprint(b, "%s@%s", s_to_c(p->s), him);
+		else
+			n = Bwrite(b, s_to_c(p->s), s_len(p->s));
+	}else
+		n = Bputc(b, p->c) == -1? -1: 1;
+	m = 0;
+	if(n != -1 && p->white)
+		m = Bwrite(b, s_to_c(p->white), s_len(p->white));
+	if(n == -1 || m == -1)
+		return nil;
+	*nbytes += n + m;
+	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 warning headers of the form
+ *	X-warning: <reason>
+ *  for any headers that looked like they might be forged.
+ *
+ *  return byte count of new headers
+ */
+static int
+forgedheaderwarnings(void)
+{
+	int nbytes;
+	Field *f;
+
+	nbytes = 0;
+
+	/* warn about envelope sender */
+	if(strcmp(s_to_c(senders.last->p), "/dev/null") != 0 &&
+	    masquerade(senders.last->p, nil))
+		nbytes += Bprint(pp->std[0]->fp,
+			"X-warning: suspect envelope domain\n");
+
+	/*
+	 *  check Sender: field.  If it's OK, ignore the others because this
+	 *  is an exploded mailing list.
+	 */
+	for(f = firstfield; f; f = f->next)
+		if(f->node->c == SENDER)
+			if(masquerade(getaddr(f->node), him))
+				nbytes += Bprint(pp->std[0]->fp,
+					"X-warning: suspect Sender: domain\n");
+			else
+				return nbytes;
+
+	/* check From: */
+	for(f = firstfield; f; f = f->next){
+		if(f->node->c == FROM && masquerade(getaddr(f->node), him))
+			nbytes += Bprint(pp->std[0]->fp,
+				"X-warning: suspect From: domain\n");
+	}
+	return nbytes;
+}
+
+static int
+parseheader(String *hdr, int *nbytesp, int *status)
+{
+	char *cp;
+	int nbytes, n;
+	Field *f;
+	Link *l;
+	Node *p;
+
+	nbytes = *nbytesp;
+	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)
+			nbytes += Bprint(pp->std[0]->fp, "From: /dev/null@%s\n", him);
+		else
+			nbytes += Bprint(pp->std[0]->fp, "From: %s\n",
+				s_to_c(senders.last->p));
+	}
+	if(destination == 0){
+		nbytes += Bprint(pp->std[0]->fp, "To: ");
+		for(l = rcvers.first; l; l = l->next){
+			if(l != rcvers.first)
+				nbytes += Bprint(pp->std[0]->fp, ", ");
+			nbytes += Bprint(pp->std[0]->fp, "%s", s_to_c(l->p));
+		}
+		nbytes += 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, &nbytes);
+		if(*status == 0 && Bprint(pp->std[0]->fp, "\n") < 0){
+			piperror = "write error";
+			*status = 1;
+		}
+		nbytes++;
+	}
+	if(cp == nil){
+		piperror = "sender domain";
+		*status = 1;
+	}
+	/* write anything we read following the header */
+	if(*status == 0){
+		n = Bwrite(pp->std[0]->fp, cp, s_to_c(hdr) + s_len(hdr) - cp);
+		if(n == -1){
+			piperror = "write error 2";
+			*status = 1;
+		}
+		nbytes += n;
+	}
+
+	*nbytesp = nbytes;
+	return *status;
+}
+
+static int
+chkhdr(char *s, int n)
+{
+	int i;
+	Rune r;
+
+	for(i = 0; i < n; ){
+		if(!fullrune(s + i, n - i))
+			return -1;
+		i += chartorune(&r, s + i);
+		if(r == Runeerror)
+			return -1;
+	}
+	return 0;
+}
+
+static void
+fancymsg(int status)
+{
+	static char msg[2*ERRMAX], *p, *e;
+
+	if(!status)
+		return;
+
+	p = seprint(msg, msg+ERRMAX, "%s: ", piperror);
+	rerrstr(p, ERRMAX);
+	piperror = msg;
+}
+
+/*
+ *  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
+ *	- elide leading dot
+ */
+int
+pipemsg(int *byteswritten)
+{
+	char *cp;
+	int n, nbytes, sawdot, status;
+	String *hdr, *line;
+	Tm tm;
+
+	pipesig(&status);	/* set status to 1 on write to closed pipe */
+	sawdot = 0;
+	status = 0;
+	werrstr("");
+	piperror = nil;
+
+	/*
+	 *  add a 'From ' line as envelope and Received: stamp
+	 */
+	nbytes = 0;
+	nbytes += Bprint(pp->std[0]->fp, "From %s %τ remote from \n",
+		s_to_c(senders.first->p), thedate(&tm));
+	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; %τ\n", me, thedate(&tm));
+
+	/*
+	 *  read first 16k obeying '.' escape.  we're assuming
+	 *  the header will all be there.
+	 */
+	line = s_new();
+	hdr = s_new();
+	while(s_len(hdr) < 16*1024){
+		n = getcrnl(s_reset(line), &bin);
+
+		/* eof or error ends the message */
+		if(n <= 0){
+			piperror = "header read error";
+			status = 1;
+			break;
+		}
+
+		cp = s_to_c(line);
+		if(chkhdr(cp, s_len(line)) == -1){
+			status = 1;
+			piperror = "mail refused: illegal header chars";
+			break;
+		}
+
+		/* a line with only a '.' ends the message */
+		if(cp[0] == '.' && cp[1] == '\n'){
+			sawdot = 1;
+			break;
+		}
+		if(cp[0] == '.'){
+			cp++;
+			n--;
+		}
+		s_append(hdr, cp);
+		nbytes += n;
+		if(*cp == '\n')
+			break;
+	}
+	if(status == 0)
+		parseheader(hdr, &nbytes, &status);
+	s_free(hdr);
+
+	/*
+	 *  pass rest of message to mailer.  take care of '.'
+	 *  escapes.
+	 */
+	for(;;){
+		n = getcrnl(s_reset(line), &bin);
+
+		/* eof or error ends the message */
+		if(n < 0){
+			piperror = "body read error";
+			status = 1;
+		}
+		if(n <= 0)
+			break;
+
+		/* a line with only a '.' ends the message */
+		cp = s_to_c(line);
+		if(cp[0] == '.' && cp[1] == '\n'){
+			sawdot = 1;
+			break;
+		}
+		if(cp[0] == '.'){
+			cp++;
+			n--;
+		}
+		nbytes += n;
+		if(status == 0 && Bwrite(pp->std[0]->fp, cp, n) < 0){
+			piperror = "write error 3";
+			status = 1;
+			break;
+		}
+	}
+	s_free(line);
+	if(status == 0 && sawdot == 0){
+		/* message did not terminate normally */
+		snprint(pipbuf, sizeof pipbuf, "network eof no dot: %r");
+		piperror = pipbuf;
+		status = 1;
+	}
+	if(status == 0 && Bflush(pp->std[0]->fp) < 0){
+		piperror = "write error 4";
+		status = 1;
+	}
+	if(status != 0)
+		syskill(pp->pid);
+	stream_free(pp->std[0]);
+	pp->std[0] = 0;
+	*byteswritten = nbytes;
+	pipesigoff();
+	if(status && piperror == nil)
+		piperror = "write on closed pipe";
+	fancymsg(status);
+	return status;
+}
+
+char*
+firstline(char *x)
+{
+	char *p;
+	static char buf[128];
+
+	strncpy(buf, x, sizeof buf);
+	buf[sizeof buf - 1] = 0;
+	p = strchr(buf, '\n');
+	if(p)
+		*p = 0;
+	return buf;
+}
+
+int
+sendermxcheck(void)
+{
+	int pid;
+	char *cp, *senddom, *user, *who;
+	Waitmsg *w;
+
+	senddom = 0;
+	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;
+		}
+		/* 4408 spf §2.2 notes that 2821 says /dev/null == postmaster@domain */
+		senddom = smprint("%s!postmaster", him);
+	}
+
+	if(access("/mail/lib/validatesender", AEXEC) < 0)
+		return 0;
+	if(!senddom)
+		senddom = strdup(who);
+	if((cp = strchr(senddom, '!')) == nil){
+		werrstr("rejected: domainless sender %s", who);
+		free(senddom);
+		return -1;
+	}
+	*cp++ = 0;
+	user = cp;
+	/* shellchars isn't restrictive.  should probablly disallow specialchars */
+	if(shellchars(senddom) || shellchars(user) || shellchars(him)){
+		werrstr("rejected: evil sender/domain/helo");
+		free(senddom);
+		return -1;
+	}
+
+	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, nci->rsys, him, 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;
+}
+
+int
+refused(char *e)
+{
+	return e && strstr(e, "mail refused") != nil;
+}
+
+/*
+ * if a message appeared on stderr, despite good status,
+ * log it.  this can happen if rewrite.in contains a bad
+ * r.e., for example.
+ */
+void
+logerrors(String *err)
+{
+	char *s;
+
+	s = s_to_c(err);
+	if(*s == 0)
+		return;
+	syslog(0, "smtpd", "%s returned good status, but said: %s",
+		s_to_c(mailer), s);
+}
+
+void
+data(void)
+{
+	char *cp, *ep, *e, buf[ERRMAX];
+	int status, nbytes;
+	Link *l;
+	String *cmd, *err;
+
+	if(rejectcheck())
+		return;
+	if(senders.last == 0){
+		reply("503 2.5.2 Data without MAIL FROM:\r\n");
+		rejectcount++;
+		return;
+	}
+	if(rcvers.last == 0){
+		reply("503 2.5.2 Data without RCPT TO:\r\n");
+		rejectcount++;
+		return;
+	}
+	if(!trusted && sendermxcheck()){
+		rerrstr(buf, sizeof buf);
+		if(strncmp(buf, "rejected:", 9) == 0)
+			reply("554 5.7.1 %s\r\n", buf);
+		else
+			reply("450 4.7.0 %s\r\n", buf);
+		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), buf);
+		rejectcount++;
+		return;
+	}
+
+	/*
+	 *  allow 145 more minutes to move the data
+	 */
+	cmd = startcmd();
+	if(cmd == 0)
+		return;
+	reply("354 Input message; end with <CRLF>.<CRLF>\r\n");
+	alarm(145*60*1000);
+	piperror = nil;
+	status = pipemsg(&nbytes);
+	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 && (refused(piperror) || refused(s_to_c(err)))){
+		filterstate = BLOCKED;
+		status = 0;
+	}
+	if(status){
+		buf[0] = 0;
+		if(piperror != nil)
+			snprint(buf, sizeof buf, "pipemesg: %s; ", piperror);
+		syslog(0, "smtpd", "++[%s/%s] %s %s %sreturned %#q %s",
+			him, nci->rsys, s_to_c(senders.first->p),
+			s_to_c(cmd), buf,
+			pp->waitmsg->msg, firstline(s_to_c(err)));
+		for(cp = s_to_c(err); ep = strchr(cp, '\n'); cp = ep){
+			*ep++ = 0;
+			reply("450-4.0.0 %s\r\n", cp);
+		}
+		reply("450 4.0.0 mail process terminated abnormally\r\n");
+		rejectcount++;
+	} else {
+		if(filterstate == BLOCKED){
+			e = firstline(s_to_c(err));
+			if(e[0] == 0)
+				e = piperror;
+			if(e == nil)
+				e = "we believe this is spam.";
+			syslog(0, "smtpd", "++[%s/%s] blocked: %s", him, nci->rsys, e);
+			reply("554 5.7.1 %s\r\n", e);
+			rejectcount++;
+		}else if(filterstate == DELAY){
+			logerrors(err);
+			reply("450 4.3.0 There will be a delay in delivery "
+				"of this message.\r\n");
+		}else{
+			logerrors(err);
+			reply("250 2.5.0 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)
+		sleep(1000 * (4<<rejectcount));
+	if(rejectcount > MAXREJECTS){
+		syslog(0, "smtpd", "Rejected (%s/%s)", him, nci->rsys);
+		reply("554 5.5.0 too many errors.  transaction failed.\r\n");
+		exits("errcount");
+	}
+	if(hardreject){
+		rejectcount++;
+		reply("554 5.7.1 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)
+{
+	int lin, lout;
+	String *sout;
+
+	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)
+{
+	int certlen, fd;
+	uchar *cert;
+	TLSconn conn;
+
+	if (tlscert == nil) {
+		reply("500 5.5.1 illegal command or bad syntax\r\n");
+		return;
+	}
+	cert = readcert(tlscert, &certlen);
+	if (cert == nil) {
+		reply("454 4.7.5 TLS not available\r\n");
+		return;
+	}
+	reply("220 2.0.0 Go ahead make my day\r\n");
+	memset(&conn, 0, sizeof(conn));
+	conn.cert = cert;
+	conn.certlen = certlen;
+	fd = tlsServer(Bfildes(&bin), &conn);
+	if (fd < 0) {
+		syslog(0, "smtpd", "TLS start-up failed with %s", him);
+		exits("tls failed");
+	}
+	Bterm(&bin);
+	if (dup(fd, 0) < 0 || dup(fd, 1) < 0)
+		fprint(2, "dup of %d failed: %r\n", fd);
+	close(fd);
+	Binit(&bin, 0, OREAD);
+	free(conn.cert);
+	free(conn.sessionID);
+	passwordinclear = 1;
+	syslog(0, "smtpd", "started TLS with %s", him);
+}
+
+int
+passauth(char *u, char *secret)
+{
+	char response[2*MD5dlen + 1];
+	uchar digest[MD5dlen];
+	int i;
+	AuthInfo *ai;
+	Chalstate *cs;
+
+	if((cs = auth_challenge("proto=cram role=server")) == nil)
+		return -1;
+	hmac_md5((uchar*)cs->chal, strlen(cs->chal),
+		(uchar*)secret, strlen(secret), digest, nil);
+	for(i = 0; i < MD5dlen; i++)
+		snprint(response + 2*i, sizeof response - 2*i, "%2.2ux", digest[i]);
+	cs->user = u;
+	cs->resp = response;
+	cs->nresp = strlen(response);
+	ai = auth_response(cs);
+	if(ai == nil)
+		return -1;
+	auth_freechal(cs);
+	auth_freeAI(ai);
+	return 0;
+}
+
+void
+auth(String *mech, String *resp)
+{
+	char *user, *pass;
+	AuthInfo *ai = nil;
+	Chalstate *chs = nil;
+	String *s_resp1_64 = nil, *s_resp2_64 = nil, *s_resp1 = nil;
+	String *s_resp2 = nil;
+
+	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 5.5.2 Bad sequence of commands\r\n");
+		goto bomb_out;
+	}
+	if(cistrcmp(s_to_c(mech), "plain") == 0){
+		if (!passwordinclear) {
+			rejectcount++;
+			reply("538 5.7.1 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 5.5.4 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;
+		authenticated = passauth(user, pass) != -1;
+		memset(pass, 'X', strlen(pass));
+		goto windup;
+	}
+	else if(cistrcmp(s_to_c(mech), "login") == 0){
+		if (!passwordinclear) {
+			rejectcount++;
+			reply("538 5.7.1 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;
+		}else
+			s_resp1_64 = resp;
+		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 5.5.4 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) {
+			/* if you authenticated, we trust you despite your IP */
+			trusted = 1;
+			reply("235 2.0.0 Authentication successful\r\n");
+		} else {
+			rejectcount++;
+			if(temperror()){
+				syslog(0, "smtpd", "temporary authentication failure: %r");
+				reply("454 4.7.0 Temporary authentication failure\r\n");
+			} else {
+				syslog(0, "smtpd", "authentication failed: %r");
+				reply("535 5.7.1 Authentication failed\r\n");
+			}
+		}
+		goto bomb_out;
+	}
+	else if(cistrcmp(s_to_c(mech), "cram-md5") == 0){
+		char *resp, *t;
+
+		chs = auth_challenge("proto=cram role=server");
+		if (chs == nil) {
+			rejectcount++;
+			if(temperror())
+				reply("454 4.7.0 Temporary authentication failure\r\n");
+			else
+				reply("501 5.7.5 Couldn't get CRAM-MD5 challenge\r\n");
+			goto bomb_out;
+		}
+		reply("334 %.*[\r\n", chs->nchal, chs->chal);
+		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 5.5.4 Cannot decode base64\r\n");
+			goto bomb_out;
+		}
+		/* should be of form <user><space><response> */
+		resp = s_to_c(s_resp1);
+		t = strchr(resp, ' ');
+		if (t == nil) {
+			rejectcount++;
+			reply("501 5.5.4 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 5.5.1 Unrecognised authentication type %s\r\n", s_to_c(mech));
+bomb_out:
+	if (ai)
+		auth_freeAI(ai);
+	if (chs)
+		auth_freechal(chs);
+	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);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtpd.h
@@ -1,0 +1,71 @@
+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;
+extern	uchar	rsysip[];
+
+int	ipcheck(char*);
+
+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);
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/smtpd.y
@@ -1,0 +1,338 @@
+%{
+#include "common.h"
+#include <ctype.h>
+#include "smtpd.h"
+
+#define YYMAXDEPTH	500		/* was default 150 */
+
+#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(); }
+		| '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("500 5.5.1 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); }
+		| '[' ipaddr ']'	={ $$ = 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; }
+		;
+ipaddr		: ipv4addr				={ $$ = cat(&$1, 0, 0, 0, 0, 0, 0); }
+		| 'i' 'p' 'v' '6' ':' ipv6addr		={ $$ = cat(&$6, 0, 0, 0, 0, 0, 0); }
+		;
+ipv6addr	: ipv6addr_list				={ $$ = cat(&$1, 0, 0, 0, 0, 0, 0); }
+		| ipv6addr_list ':' ':' ipv6addr_list	={ $$ = cat(&$1, &$2, &$3, &$4, 0, 0, 0); }
+		| ':' ':' ipv6addr_list			={ $$ = cat(&$1, &$2, &$3, 0, 0, 0, 0); }
+		;
+ipv6addr_list	: ipv6addr_elem				={ $$ = cat(&$1, 0, 0, 0, 0, 0, 0); }
+		| ipv6addr_list ':' ipv6addr_elem	={ $$ = cat(&$1, &$2, &$3, 0, 0, 0, 0); }
+		;
+ipv6addr_elem	: hnum					={ $$ = cat(&$1, 0, 0, 0, 0, 0, 0); }
+		| ipv4addr				={ $$ = cat(&$1, 0, 0, 0, 0, 0, 0); }
+		;
+ipv4addr	: 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) fprint(2, "bad snum\n"); }
+		;
+hnum		: h		={ $$ = cat(&$1, 0, 0, 0, 0, 0, 0); }
+		| h h		={ $$ = cat(&$1, &$2, 0, 0, 0, 0, 0); }
+		| h h h		={ $$ = cat(&$1, &$2, &$3, 0, 0, 0, 0); }
+		| h h h h	={ $$ = cat(&$1, &$2, &$3, &$4, 0, 0, 0); }
+		;
+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'
+		;
+h		: d | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
+		;
+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;
+	return rv;
+}
+
+void
+yyerror(char *x)
+{
+	USED(x);
+}
+
+/*
+ *  an anonymous user
+ */
+YYSTYPE
+anonymous(void)
+{
+	YYSTYPE rv;
+
+	rv.s = s_copy("/dev/null");
+	return rv;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/smtp/spam.c
@@ -1,0 +1,547 @@
+#include "common.h"
+#include "smtpd.h"
+#include <ip.h>
+
+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	char*	getline(Biobuf*);
+
+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)
+{
+	static 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[Pathlen];
+
+	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];
+
+	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:
+			while(trusted == 0 && *p){
+				trusted = ipcheck(p);
+				p += strlen(p) + 1;
+			}
+			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 \<newline> */
+				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;
+}
+
+int
+isbadguy(void)
+{
+	Link *l;
+
+	/* check if this IP address is banned */
+	for(l = badguys.first; l; l = l->next)
+		if(ipcheck(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;
+	Tm tm;
+	ulong h;
+	static char buf[512];
+	char *cp, mon[8], day[4];
+
+	if (sflag == 1){
+		snprint(mon, sizeof(mon), "%τ", tmfmt(&tm, "MMM"));
+		snprint(day, sizeof(day), "%τ", tmfmt(&tm, "D"));
+		snprint(buf, sizeof buf, "%s/queue.dump/%s%s", SPOOL, mon, day);
+		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 = create(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;
+}
--- /dev/null
+++ b/sys/src/cmd/upas/spf/dns.c
@@ -1,0 +1,81 @@
+#include "spf.h"
+
+extern char	dflag;
+extern char	vflag;
+extern char	*netroot;
+
+static int
+timeout(void*, char *msg)
+{
+	if(strstr(msg, "alarm")){
+		fprint(2, "deferred: dns timeout");
+		exits("deferred: dns timeout");
+	}
+	return 0;
+}
+
+static Ndbtuple*
+tdnsquery(char *r, char *s, char *v)
+{
+	long a;
+	Ndbtuple *t;
+
+	atnotify(timeout, 1);
+	a = alarm(15*1000);
+	t = dnsquery(r, s, v);
+	alarm(a);
+	atnotify(timeout, 0);
+	return t;
+}
+
+Ndbtuple*
+vdnsquery(char *s, char *v, int recur)
+{
+	Ndbtuple *n, *t;
+	static int nquery;
+
+	/* conflicts with standard: must limit to 10 and -> fail */
+	if(recur > 5 || ++nquery == 25){
+		fprint(2, "dns query limited %d %d\n", recur, nquery);
+		return 0;
+	}
+	if(dflag)
+		fprint(2, "dnsquery(%s, %s, %s) ->\n", netroot, s, v);
+	t = tdnsquery(netroot, s, v);
+	if(dflag)
+		for(n = t; n; n = n->entry)
+			fprint(2, "\t%s\t%s\n", n->attr, n->val);
+	return t;
+}
+
+void
+dnreverse(char *s, int l, char *d)
+{
+	char *p, *e, buf[100], *f[15];
+	int i, n;
+
+	n = getfields(d, f, nelem(f), 0, ".");
+	p = e = buf;
+	if(l < sizeof buf)
+		e += l;
+	else
+		e += sizeof buf;
+	for(i = 1; i <= n; i++)
+		p = seprint(p, e, "%s.", f[n-i]);
+	if(p > buf)
+		p = seprint(p-1, e, ".in-addr.arpa");
+	memmove(s, buf, p-buf+1);
+}
+
+int
+dncontains(char *d, char *s)
+{
+loop:
+	if(!strcmp(d, s))
+		return 1;
+	if(!(s = strchr(s, '.')))
+		return 0;
+	s++;
+	goto loop;	
+}
+
--- /dev/null
+++ b/sys/src/cmd/upas/spf/macro.c
@@ -1,0 +1,304 @@
+#include "spf.h"
+
+#define mrprint(...)	snprint(m->mreg, sizeof m->mreg, __VA_ARGS__)
+
+typedef struct Mfmt Mfmt;
+typedef struct Macro Macro;
+
+struct Mfmt{
+	char	buf[0xff];
+	char	*p;
+	char	*e;
+
+	char	mreg[0xff];
+	int	f1;
+	int	f2;
+	int	f3;
+
+	char	*sender;
+	char	*domain;
+	char	*ip;
+	char	*helo;
+	uchar	ipa[IPaddrlen];
+};
+
+struct Macro{
+	char	c;
+	void	(*f)(Mfmt*);
+};
+
+static void
+ms(Mfmt *m)
+{
+	mrprint("%s", m->sender);
+}
+
+static void
+ml(Mfmt *m)
+{
+	char *p;
+
+	mrprint("%s", m->sender);
+	if(p = strchr(m->mreg, '@'))
+		*p = 0;
+}
+
+static void
+mo(Mfmt *m)
+{
+	mrprint("%s", m->domain);
+}
+
+static void
+md(Mfmt *m)
+{
+	mrprint("%s", m->domain);
+}
+
+static void
+mi(Mfmt *m)
+{
+	uint i, c;
+
+	if(isv4(m->ipa))
+		mrprint("%s", m->ip);
+	else{
+		for(i = 0; i < 32; i++){
+			c = m->ipa[i / 2];
+			if((i & 1) == 0)
+				c >>= 4;
+			sprint(m->mreg+2*i, "%ux.", c & 0xf);
+		}
+		m->mreg[2*32 - 1] = 0;
+	}
+}
+
+static int
+maquery(Mfmt *m, char *d, char *match, int recur)
+{
+	int r;
+	Ndbtuple *t, *n;
+
+	r = 0;
+	t = vdnsquery(d, "any", recur);
+	for(n = t; n; n = n->entry)
+		if(!strcmp(n->attr, "ip") || !strcmp(n->attr, "ipv6")){
+			if(!strcmp(n->val, match)){
+				r = 1;
+				break;
+			}
+		}else if(!strcmp(n->attr, "cname"))
+			maquery(m, d, match, recur+1);
+	ndbfree(t);
+	return r;
+}
+
+static int
+lrcmp(char *a, char *b)
+{
+	return strlen(b) - strlen(a);
+}
+
+static void
+mptrquery(Mfmt *m, char *d, int recur)
+{
+	char *s, buf[64], *a, *list[11];
+	int nlist, i;
+	Ndbtuple *t, *n;
+
+	nlist = 0;
+	dnreverse(buf, sizeof buf, s = strdup(m->ip));
+	t = vdnsquery(buf, "ptr", recur);
+	for(n = t; n; n = n->entry){
+		if(!strcmp(n->attr, "dom") || !strcmp(n->attr, "cname"))
+		if(dncontains(n->val, d) && maquery(m, n->val, m->ip, recur+1))
+			list[nlist++] = strdup(n->val);
+	}
+	ndbfree(t);
+	free(s);
+	qsort(list, nlist, sizeof *list, (int(*)(void*,void*))lrcmp);
+	a = "unknown";
+	for(i = 0; i < nlist; i++)
+		if(!strcmp(list[i], d)){
+			a = list[i];
+			break;
+		}else if(dncontains(list[i], d))
+			a = list[i];
+	mrprint("%s", a);
+	for(i = 0; i < nlist; i++)
+		free(list[i]);
+}
+
+static void
+mp(Mfmt *m)
+{
+	/*
+	 * we're supposed to do a reverse lookup on the ip & compare.
+	 * this is a very bad idea.
+	 */
+//	mrprint("unknown);	/* simulate dns failure */
+	mptrquery(m, m->domain, 0);
+}
+
+static void
+mv(Mfmt *m)
+{
+	if(isv4(m->ipa))
+		mrprint("in-addr");
+	else
+		mrprint("ip6");
+}
+
+static void
+mh(Mfmt *m)
+{
+	mrprint("%s", m->helo);
+}
+
+static Macro tab[] = {
+'s',	ms,	/* sender */
+'l',	ml,	/* local part of sender */
+'o',	mo,	/* domain of sender */
+'d',	md,	/* domain */
+'i',	mi,	/* ip */
+'p',	mp,	/* validated domain name of ip */
+'v',	mv,	/* "in-addr" if ipv4, or "ip6" if ipv6 */
+'h',	mh,	/* helo/ehol domain */
+};
+
+static void
+reverse(Mfmt *m)
+{
+	char *p, *e, buf[100], *f[32], sep[2];
+	int i, n;
+
+	sep[0] = m->f2;
+	sep[1] = 0;
+	n = getfields(m->mreg, f, nelem(f), 0, sep);
+	p = e = buf;
+	e += sizeof buf-1;
+	for(i = 0; i < n; i++)
+		p = seprint(p, e, "%s.", f[n-i-1]);
+	if(p > buf)
+		p--;
+	*p = 0;
+	memmove(m->mreg, buf, p-buf+1);
+	m->f2 = '.';
+}
+
+static void
+chop(Mfmt *m)
+{
+	char *p, *e, buf[100], *f[32], sep[2];
+	int i, n;
+
+	sep[0] = m->f2;
+	sep[1] = 0;
+	n = getfields(m->mreg, f, nelem(f), 0, sep);
+	p = e = buf;
+	e += sizeof buf-1;
+	if(m->f1 == 0)
+		i = 0;
+	else
+		i = n-m->f1;
+	if(i < 0)
+		i = 0;
+	for(; i < n; i++)
+		p = seprint(p, e, "%s.", f[i]);
+	if(p > buf)
+		p--;
+	*p = 0;
+	memmove(m->mreg, buf, p-buf+1);
+	m->f2 = '.';
+}
+
+static void
+mfmtinit(Mfmt *m, char *s, char *d, char *h, char *i)
+{
+	memset(m, 0, sizeof *m);
+	m->p = m->buf;
+	m->e = m->p + sizeof m->buf-1;
+	m->sender = s? s: "Unsets";
+	m->domain = d? d: "Unsetd";
+	m->helo = h? h: "Unseth";
+	m->ip = i? i: "127.0.0.2";
+	parseip(m->ipa, m->ip);
+}
+
+/* url escaping? rfc3986 */
+static void
+mputc(Mfmt *m, int c)
+{
+	if(m->p < m->e)
+		*m->p++ = c;
+}
+
+static void
+mputs(Mfmt *m, char *s)
+{
+	int c;
+
+	while(c = *s++)
+		mputc(m, c);
+}
+
+char*
+macro(char *f, char *sender, char *dom, char *hdom, char *ip)
+{
+	char *p;
+	int i, c;
+	Mfmt m;
+
+	mfmtinit(&m, sender, dom, hdom, ip);
+	while(*f){
+		while((c = *f++) && c != '%')
+			mputc(&m, c);
+		if(c == 0)
+			break;
+		switch(*f++){
+		case '%':
+			mputc(&m, '%');
+			break;
+		case '-':
+			mputs(&m, "%20");
+			break;
+		case '_':
+			mputc(&m, ' ');
+			break;
+		case '{':
+			m.f1 = 0;
+			m.f2 = '.';
+			m.f3 = 0;
+			c = *f++;
+			if(c >= 'A' && c <= 'Z')
+				c += 0x20;
+			for(i = 0; i < nelem(tab); i++)
+				if(tab[i].c == c)
+					break;
+			if(i == nelem(tab))
+				return 0;
+			for(c = *f++; c >= '0' && c <= '9'; c = *f++)
+				m.f1 = m.f1*10 + c-'0';
+			if(c == 'R' || c == 'r'){
+				m.f3 = 'r';
+				c = *f++;
+			}
+			for(; p = strchr(".-+,_=", c); c = *f++)
+				m.f2 = *p;
+			if(c == '}'){
+				tab[i].f(&m);
+				if(m.f1 || m.f2 != '.')
+					chop(&m);
+				if(m.f3 == 'r')
+					reverse(&m);
+				mputs(&m, m.mreg);
+				m.mreg[0] = 0;
+				break;
+			}
+		default:
+			return 0;
+		}
+	}
+	mputc(&m, 0);
+	return strdup(m.buf);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/spf/mkfile
@@ -1,0 +1,14 @@
+</$objtype/mkfile
+
+TARG=spf
+
+OFILES=\
+	dns.$O\
+	macro.$O\
+	spf.$O\
+
+</sys/src/cmd/mkone
+<../mkupas
+
+mtest: dns.$O macro.$O mtest.$O
+	$LD $LDFLAGS -o $target $prereq
--- /dev/null
+++ b/sys/src/cmd/upas/spf/mtest.c
@@ -1,0 +1,39 @@
+#include "spf.h"
+
+char	dflag;
+char	vflag;
+char	*netroot = "/net";
+
+void
+usage(void)
+{
+	fprint(2, "usage: mtest [-dv] sender dom hello ip\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *a[5], *s;
+	int i;
+
+	ARGBEGIN{
+	case 'd':
+		dflag = 1;
+		break;
+	case 'v':
+		vflag = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	fmtinstall('I', eipfmt);
+	memset(a, 0, sizeof a);
+	for(i = 0; i < argc && i < nelem(a); i++)
+		a[i] = argv[i];
+	s = macro(a[0], a[1], a[2], a[3], a[4]);
+	print("%s\n", s);
+	free(s);
+	exits("");
+}
--- /dev/null
+++ b/sys/src/cmd/upas/spf/spf.c
@@ -1,0 +1,726 @@
+#include "spf.h"
+
+#define	vprint(...) if(vflag) fprint(2, __VA_ARGS__)
+
+enum{
+	Traw,
+	Tip4,
+	Tip6,
+	Texists,
+	Tall,
+	Tbegin,
+	Tend,
+};
+
+char *typetab[] = {
+	"raw",
+	"ip4",
+	"ip6",
+	"exists",
+	"all",
+	"begin",
+	"end",
+};
+
+typedef struct Squery Squery;
+struct Squery{
+	char	ver;
+	char	sabort;
+	char	mod;
+	char	*cidrtail;
+	char	*ptrmatch;
+	char	*ip;
+	char	*domain;
+	char	*sender;
+	char	*hello;
+};
+
+typedef struct Spf Spf;
+struct Spf{
+	char	mod;
+	char	type;
+	char	s[100];
+};
+#pragma	varargck type	"§"	Spf*
+
+char	*txt;
+char	*netroot = "/net";
+char	dflag;
+char	eflag;
+char	mflag;
+char	pflag;
+char	rflag;
+char	vflag;
+
+char *vtab[] = {0, "v=spf1", "spf2.0/"};
+
+char*
+isvn(Squery *q, char *s, int i)
+{
+	char *p, *t;
+
+	t = vtab[i];
+	if(cistrncmp(s, t, strlen(t)))
+		return 0;
+	p = s + strlen(t);
+	if(i == 2){
+		p = strchr(p, ' ');
+		if(p == nil)
+			return 0;
+	}
+	if(*p && *p++ != ' ')
+		return 0;
+	q->ver = i;
+	return p;
+}
+
+char*
+pickspf(Squery *s, char *v1, char *v2)
+{
+	switch(s->ver){
+	default:
+	case 0:
+		if(v1)
+			return v1;
+		return v2;
+	case 1:
+		if(v1)
+			return v1;
+		return 0;
+	case 2:
+		if(v2)
+			return v2;
+		return v1;	/* spf2.0/pra,mfrom */
+	}
+}
+
+char *ftab[] = {"txt", "spf"};	/* p. 9 */
+
+char*
+spffetch(Squery *s, char *d)
+{
+	char *p, *v1, *v2;
+	int i;
+	Ndbtuple *t, *n;
+
+	if(txt){
+		p = strdup(txt);
+		txt = 0;
+		return p;
+	}
+	v1 = v2 = 0;
+	for(i = 0; i < nelem(ftab); i++){
+		t = vdnsquery(d, ftab[i], 0);
+		for(n = t; n; n = n->entry){
+			if(strcmp(n->attr, ftab[i]))
+				continue;
+			v1 = isvn(s, n->val, 1);
+			v2 = isvn(s, n->val, 2);
+		}
+		if(p = pickspf(s, v1, v2))
+			p = strdup(p);
+		ndbfree(t);
+		if(p)
+			return p;
+	}
+	return 0;
+}
+
+Spf	spftab[200];
+int	nspf;
+int	mod;
+
+Spf*
+spfadd(int type, char *s)
+{
+	Spf *p;
+
+	if(nspf >= nelem(spftab))
+		return 0;
+	p = spftab+nspf;
+	p->s[0] = 0;
+	if(s)
+		snprint(p->s, sizeof p->s, "%s", s);
+	p->type = type;
+	p->mod = mod;
+	nspf++;
+	return p;
+}
+
+int
+parsecidr(uchar *addr, uchar *mask, char *from)
+{
+	char *p, buf[50];
+	int i, bits, z;
+	vlong v;
+	uchar *a;
+
+	strecpy(buf, buf+sizeof buf, from);
+	if(p = strchr(buf, '/'))
+		*p = 0;
+	v = parseip(addr, buf);
+	if(v == -1)
+		return -1;
+	switch((ulong)v){
+	default:
+		bits = 32;
+		z = 96;
+		break;
+	case 6:
+		bits = 128;
+		z = 0;
+		break;
+	}
+
+	if(p){
+		i = strtoul(p+1, &p, 0);
+		if(i > bits)
+			i = bits;
+		i += z;
+		memset(mask, 0, 128/8);
+		for(a = mask; i >= 8; i -= 8)
+			*a++ = 0xff;
+		if(i > 0)
+			*a = ~((1<<(8-i))-1);
+	}else
+		memset(mask, 0xff, IPaddrlen);
+	return 0;
+}
+
+/*
+ * match x.y.z.w to x1.y1.z1.w1/m
+ */
+int
+cidrmatch(char *x, char *y)
+{
+	uchar a[IPaddrlen], b[IPaddrlen], m[IPaddrlen];
+
+	if(parseip(a, x) == -1)
+		return 0;
+	parsecidr(b, m, y);
+	maskip(a, m, a);
+	maskip(b, m, b);
+	if(!memcmp(a, b, IPaddrlen))
+		return 1;
+	return 0;
+}
+
+int
+ptrmatch(Squery *q, char *s)
+{
+	if(!q->ptrmatch || !strcmp(q->ptrmatch, s))
+		return 1;
+	return 0;
+}
+
+Spf*
+spfaddcidr(Squery *q, int type, char *s)
+{
+	char buf[64];
+
+	if(q->cidrtail){
+		snprint(buf, sizeof buf, "%s/%s", s, q->cidrtail);
+		s = buf;
+	}
+	if(ptrmatch(q, s))
+		return spfadd(type, s);
+	return 0;
+}
+
+char*
+qpluscidr(Squery *q, char *d, int recur, int *y)
+{
+	char *p;
+
+	*y = 0;
+	if(!recur && (p = strchr(d, '/'))){
+		q->cidrtail = p + 1;
+		*p = 0;
+		*y = 1;
+	}
+	return d;
+}
+
+void
+cidrtail(Squery *q, char *, int y)
+{
+	if(!y)
+		return;
+	q->cidrtail[-1] = '/';
+	q->cidrtail = 0;
+}
+
+void
+aquery(Squery *q, char *d, int recur)
+{
+	int y;
+	Ndbtuple *t, *n;
+
+	d = qpluscidr(q, d, recur, &y);
+	t = vdnsquery(d, "any", recur);
+	for(n = t; n; n = n->entry){
+		if(!strcmp(n->attr, "ip"))
+			spfaddcidr(q, Tip4, n->val);
+		else if(!strcmp(n->attr, "ipv6"))
+			spfaddcidr(q, Tip6, n->val);
+		else if(!strcmp(n->attr, "cname"))
+			aquery(q, d, recur+1);
+	}
+	cidrtail(q, d, y);
+	ndbfree(t);
+}
+
+void
+mxquery(Squery *q, char *d, int recur)
+{
+	int i, y;
+	Ndbtuple *t, *n;
+
+	d = qpluscidr(q, d, recur, &y);
+	i = 0;
+	t = vdnsquery(d, "mx", recur);
+	for(n = t; n; n = n->entry)
+		if(i++ < 10 && !strcmp(n->attr, "mx"))
+			aquery(q, n->val, recur+1);
+	ndbfree(t);
+	cidrtail(q, d, y);
+}
+
+void
+ptrquery(Squery *q, char *d, int recur)
+{
+	char *s, buf[64];
+	int i, y;
+	Ndbtuple *t, *n;
+
+	if(!q->ip){
+		fprint(2, "spf: ptr query; no ip\n");
+		return;
+	}
+	d = qpluscidr(q, d, recur, &y);
+	i = 0;
+	dnreverse(buf, sizeof buf, s = strdup(q->ip));
+	t = vdnsquery(buf, "ptr", recur);
+	for(n = t; n; n = n->entry){
+		if(!strcmp(n->attr, "dom") || !strcmp(n->attr, "cname"))
+		if(i++ < 10 && dncontains(d, n->val)){
+			q->ptrmatch = q->ip;
+			aquery(q, n->val, recur+1);
+			q->ptrmatch = 0;
+		}
+	}
+	ndbfree(t);
+	free(s);
+	cidrtail(q, d, y);
+}
+
+/*
+ * this looks very wrong; see §5.7 which says only a records match.
+ */
+void
+exists(Squery*, char *d, int recur)
+{
+	Ndbtuple *t;
+
+	if(t = vdnsquery(d, "ip", recur))
+		spfadd(Texists, "1");
+	else
+		spfadd(Texists, 0);
+	ndbfree(t);
+}
+
+void
+addfail(void)
+{
+	mod = '-';
+	spfadd(Tall, 0);
+}
+
+void
+addend(char *s)
+{
+	spfadd(Tend, s);
+	spftab[nspf-1].mod = 0;
+}
+
+Spf*
+includeloop(char *s1, int n)
+{
+	char *s, *p;
+	int i;
+
+	for(i = 0; i < n; i++){
+		s = spftab[i].s;
+		if(s)
+		if(p = strstr(s, " -> "))
+		if(!strcmp(p+4, s1))
+			return spftab+i;
+	}
+	return nil;
+}
+
+void
+addbegin(int c, char *s0, char *s1)
+{
+	char buf[0xff];
+
+	snprint(buf, sizeof buf, "%s -> %s", s0, s1);
+	spfadd(Tbegin, buf);
+	spftab[nspf-1].mod = c;
+}
+
+void
+ditch(void)
+{
+	if(nspf > 0)
+		nspf--;
+}
+
+static void
+lower(char *s)
+{
+	int c;
+
+	for(; c = *s; s++)
+		if(c >= 'A' && c <= 'Z')
+			*s = c + 0x20;
+}
+
+int
+spfquery(Squery *x, char *d, int include, int depth)
+{
+	char *s, **t, *r, *p, *q, buf[10];
+	int i, n, c;
+	Spf *inc;
+
+	if(include)
+	if(inc = includeloop(d, nspf-1)){
+		fprint(2, "spf: include loop: %s (%s)\n", d, inc->s);
+		return -1;
+	}
+	if(depth >= 10){
+		fprint(2, "spf: too much recursion %s\n", d);
+		return -1;
+	}
+	s = spffetch(x, d);
+	if(!s)
+		return -1;
+	t = malloc(500*sizeof *t);
+	n = getfields(s, t, 500, 1, " ");
+	x->sabort = 0;
+	for(i = 0; i < n && !x->sabort; i++){
+		if(!strncmp(t[i], "v=", 2))
+			continue;
+		c = *t[i];
+		r = t[i]+1;
+		switch(c){
+		default:
+			mod = '+';
+			r--;
+			break;
+		case '-':
+		case '~':
+		case '+':
+		case '?':
+			mod = c;
+			break;
+		}
+		if(!strcmp(r, "all")){
+			spfadd(Tall, 0);
+			continue;
+		}
+		strecpy(buf, buf+sizeof buf, r);
+		p = strchr(buf, ':');
+		if(p == 0)
+			p = strchr(buf, '=');
+		q = d;
+		if(p){
+			*p = 0;
+			q = p+1;
+			q = r+(q-buf);
+		}
+		if(!mflag)
+			q = macro(q, x->sender, x->domain, x->hello, x->ip);
+		else
+			q = strdup(q);
+		lower(buf);
+		if(!strcmp(buf, "ip4"))
+			spfaddcidr(x, Tip4, q);
+		else if(!strcmp(buf, "ip6"))
+			spfaddcidr(x, Tip6, q);
+		else if(!strcmp(buf, "a"))
+			aquery(x, q, 0);
+		else if(!strcmp(buf, "mx"))
+			mxquery(x, d, 0);
+		else if(!strcmp(buf, "ptr"))
+			ptrquery(x, d, 0);
+		else if(!strcmp(buf, "exists"))
+			exists(x, q, 0);
+		else if(!strcmp(buf, "include") || !strcmp(buf, "redirect")){
+			if(q && *q){
+				if(rflag)
+					fprint(2, "I> %s\n", q);
+				addbegin(mod, r, q);
+				if(spfquery(x, q, 1, depth+1) == -1){
+					ditch();
+					addfail();
+				}else
+					addend(r);
+			}
+		}
+		free(q);
+	}
+	free(t);
+	free(s);
+	return 0;
+}
+
+char*
+url(char *s)
+{
+	char buf[64], *p, *e;
+	int c;
+
+	p = buf;
+	e = p + sizeof buf;
+	*p = 0;
+	while(c = *s++){
+		if(c >= 'A' && c <= 'Z')
+			c += 0x20;
+		if(c <= ' ' || c == '%' || c & 0x80)
+			p = seprint(p, e, "%%%2.2X", c);
+		else
+			p = seprint(p, e, "%c", c);
+	}
+	return strdup(buf);
+}
+
+void
+spfinit(Squery *q, char *dom, int argc, char **argv)
+{
+	uchar a[IPaddrlen];
+
+	memset(q, 0, sizeof q);
+	q->ip = argc>0? argv[1]: 0;
+	if(q->ip && parseip(a, q->ip) == -1)
+		sysfatal("bogus ip");
+	q->domain = url(dom);
+	q->sender = argc>2? url(argv[2]): 0;
+	q->hello = argc>3? url(argv[3]): 0;
+	mod = 0;				/* BOTCH */
+}
+
+int
+§fmt(Fmt *f)
+{
+	char *p, *e, buf[115];
+	Spf *spf;
+
+	spf = va_arg(f->args, Spf*);
+	if(!spf)
+		return fmtstrcpy(f, "<nil>");
+	e = buf+sizeof buf;
+	p = buf;
+	if(spf->mod && spf->mod != '+')
+		*p++ = spf->mod;
+	p = seprint(p, e, "%s", typetab[spf->type]);
+	if(spf->s[0])
+		seprint(p, e, " : %s", spf->s);
+	return fmtstrcpy(f, buf);
+}
+
+static Spf head;
+
+struct{
+	int	i;
+}walk;
+
+int
+invertmod(int c)
+{
+	switch(c){
+	case '?':
+		return '?';
+	case '+':
+		return '-';
+	case '-':
+		return '+';
+	case '~':
+		return '?';
+	}
+	return 0;
+}
+
+#define reprint(...) if(vflag && recur == 0) fprint(2, __VA_ARGS__)
+
+int
+spfwalk(int all, int recur, char *ip)
+{
+	int match, bias, mod, r;
+	Spf *s;
+
+	r = 0;
+	bias = 0;
+	if(recur == 0)
+		walk.i = 0;
+	for(; walk.i < nspf; walk.i++){
+		s = spftab+walk.i;
+		mod = s->mod;
+		switch(s->type){
+		default:
+			abort();
+		case Tbegin:
+			walk.i++;
+			match = spfwalk(s->s[0] == 'r', recur+1, ip);
+			if(match < 0)
+				mod = invertmod(mod);
+			break;
+		case Tend:
+			return r;
+		case Tall:
+			match = 1;
+			break;
+		case Texists:
+			match = s->s[0];
+			break;
+		case Tip4:
+		case Tip6:
+			match = cidrmatch(ip, s->s);
+			break;
+		}
+		if(!r && match)
+			switch(mod){
+			case '~':
+				reprint("bias %§\n", s);
+				bias = '~';
+			case '?':
+				break;
+			case '-':
+				if(all || s->type !=Tall){
+					vprint("fail %§\n", s);
+					r = -1;
+				}
+				break;
+			case '+':
+			default:
+				vprint("match %§\n", s);
+				r = 1;
+			}
+	}
+	/* recur == 0 */
+	if(r == 0 && bias == '~')
+		r = -1;
+	return r;
+}
+
+/* ad hoc and noncomprehensive */
+char *tccld[] = {"au", "ca", "gt", "id", "pk",  "uk", "ve", };
+int
+is3cctld(char *s)
+{
+	int i;
+
+	if(strlen(s) != 2)
+		return 0;
+	for(i = 0; i < nelem(tccld); i++)
+		if(!strcmp(tccld[i], s))
+			return 1;
+	return 0;
+}
+
+char*
+rootify(char *d)
+{
+	char *p, *q;
+
+	if(!(p = strchr(d, '.')))
+		return 0;
+	p++;
+	if(!(q = strchr(p, '.')))
+		return 0;
+	q++;
+	if(!strchr(q, '.') && is3cctld(q))
+		return 0;
+	return p;
+}
+
+void
+usage(void)
+{
+	fprint(2, "spf [-demrpv] [-n netroot] dom [ip sender helo]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *s, *d, *e;
+	int i, j, t[] = {0, 3};
+	Squery q;
+
+	ARGBEGIN{
+	case 'd':
+		dflag = 1;
+		break;
+	case 'e':
+		eflag = 1;
+		break;
+	case 'm':
+		mflag = 1;
+		break;
+	case 'n':
+		netroot = EARGF(usage());
+		break;
+	case 'p':
+		pflag = 1;
+		break;
+	case 'r':
+		rflag = 1;
+		break;
+	case 't':
+		txt = EARGF(usage());
+		break;
+	case 'v':
+		vflag = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc < 1 || argc > 4)
+		usage();
+	if(argc == 1)
+		pflag = 1;
+	fmtinstall(L'§', §fmt);
+	fmtinstall('I', eipfmt);
+	fmtinstall('M', eipfmt);
+
+	e = "none";
+	for(i = 0; i < nelem(t); i++){
+		if(argc <= t[i])
+			break;
+		d = argv[t[i]];
+		for(j = 0; j < i; j++)
+			if(!strcmp(argv[t[j]], d))
+				goto loop;
+		for(s = d; ; s = rootify(s)){
+			if(!s)
+				goto loop;
+			spfinit(&q, d, argc, argv);	/* or s? */
+			addbegin('+', ".", s);
+			if(spfquery(&q, s, 0, 0) != -1)
+				break;
+		}
+		if(eflag && nspf)
+			addfail();
+		e = "";
+		if(pflag)
+		for(j = 0; j < nspf; j++)
+			print("%§\n", spftab+j);
+		if(argc >= t[i] && argc > 1)
+		if(spfwalk(1, 0, argv[1]) == -1)
+			exits("fail");
+loop:;
+	}
+	exits(e);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/spf/spf.h
@@ -1,0 +1,12 @@
+/* © 2008 erik quanstrom; plan 9 license */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include <ip.h>
+
+char		*macro(char*, char*, char*, char*, char*);
+
+Ndbtuple*	vdnsquery(char*, char*, int);
+int		dncontains(char*, char *);
+void		dnreverse(char*, int, char*);
--- /dev/null
+++ b/sys/src/cmd/upas/spf/testsuite
@@ -1,0 +1,9 @@
+#!/bin/rc
+for(i in '%{s}' '%{o}' '%{d}' '%{d4}' '%{d3}' '%{d2}' '%{d1}' '%{dr}' '%{d2r}' '%{l}' '%{l-}' '%{lr}' '%{lr-}' '%{l1r-}')
+	mtest $i '[email protected]' email.example.com helounknown 192.0.2.3 
+for(i in '%{i}')
+	mtest $i '[email protected]' email.example.com helounknown 2001:db8::cb01
+for(i in '%{ir}.%{v}._spf.%{d2}' '%{lr-}.lp._spf.%{d2}' '%{lr-}.lp.%{ir}.%{v}._spf.%{d2}' '%{ir}.%{v}.%{lr-}.lp._spf.%{d2}')	
+	mtest $i '[email protected]' email.example.com helounknown 2001:db8::cb01
+for(i in '%{ir}.%{v}._spf.%{d2}' '%{lr-}.lp._spf.%{d2}' '%{lr-}.lp.%{ir}.%{v}._spf.%{d2}' '%{ir}.%{v}.%{lr-}.lp._spf.%{d2}')	
+	mtest $i '[email protected]' email.example.com helounknown 192.0.2.3 
--- /dev/null
+++ b/sys/src/cmd/upas/unesc/mkfile
@@ -1,0 +1,7 @@
+</$objtype/mkfile
+
+TARG=unesc
+OFILES=unesc.$O
+
+</sys/src/cmd/mkone
+<../mkupas
--- /dev/null
+++ b/sys/src/cmd/upas/unesc/unesc.c
@@ -1,0 +1,52 @@
+/*
+ *	upas/unesc - interpret =?foo?bar?=char?= escapes
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+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(void)
+{
+	int c;
+	Biobuf bin, bout;
+
+	Binit(&bin,  0, OREAD);
+	Binit(&bout, 1, OWRITE);
+	while((c = Bgetc(&bin)) != Beof)
+		if(c != '=')
+			Bputc(&bout, c);
+		else if((c = Bgetc(&bin)) != '?'){
+			Bputc(&bout, '=');
+			Bputc(&bout, c);
+		} else {
+			while((c = Bgetc(&bin)) != Beof && c != '?')
+				continue;		/* consume foo */
+			while((c = Bgetc(&bin)) != Beof && c != '?')
+				continue;		/* consume bar */
+			while((c = Bgetc(&bin)) != Beof && c != '?'){
+				if(c == '='){
+					c  = hex(Bgetc(&bin)) << 4;
+					c |= hex(Bgetc(&bin));
+				}
+				Bputc(&bout, c);
+			}
+			c = Bgetc(&bin);		/* consume '=' */
+			if (c != '=')	
+				Bungetc(&bin);
+		}
+	Bterm(&bout);
+	exits(0);
+}
--- /dev/null
+++ b/sys/src/cmd/upas/vf/mkfile
@@ -1,0 +1,12 @@
+</$objtype/mkfile
+
+TARG=vf
+LIB=../common/libcommon.a$O
+OFILES=vf.$O
+HFILES=\
+	../common/common.h\
+	../common/sys.h\
+
+</sys/src/cmd/mkone
+<../mkupas
+CFLAGS=$CFLAGS -I../common
--- /dev/null
+++ b/sys/src/cmd/upas/vf/vf.c
@@ -1,0 +1,1122 @@
+/*
+ *  this is a filter that changes mime types and names of
+ *  suspect executable attachments.
+ */
+#include "common.h"
+#include <ctype.h>
+
+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
+usage(void)
+{
+	fprint(2, "usage: upas/vf [-r] [-s savefile]\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	ARGBEGIN{
+	case 'r':
+		justreject = 1;
+		break;
+	case 's':
+		savefile = EARGF(usage());
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc)
+		usage();
+
+	tmfmtinstall();
+
+	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(char *reason)
+{
+	char *full;
+	static char msg[] =
+		"mail refused: we don't accept executable attachments";
+
+	full = smprint("%s: %s", msg, reason);
+	postnote(PNGROUP, getpid(), full);
+	exits(full);
+}
+
+
+/*
+ *  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(nil);
+				}
+				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 = &in;
+			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));
+	}
+}
+
+/*
+ *  save the message somewhere
+ */
+static vlong bodyoff;	/* clumsy hack */
+
+static int
+save(Part *p, char *file)
+{
+	int fd;
+	Tm tm;
+
+	Bterm(&out);
+	memset(&out, 0, sizeof(out));
+
+	fd = open(file, OWRITE);
+	if(fd < 0)
+		return -1;
+	seek(fd, 0, 2);
+	Binit(&out, fd, OWRITE);
+	Bprint(&out, "From virusfilter %τ\n", thedate(&tm));
+	writeheader(p, 0);
+	bodyoff = Boffset(&out);
+	passbody(p, 0);
+	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 *name;
+	int fd;
+
+	name = mktemp(smprint("%s/vf.XXXXXXXXXXX", UPASTMP));
+	if((fd = create(name, OWRITE|OEXCL, 0666)) < 0){
+		fprint(2, "%s: error creating temporary file: %r\n", argv0);
+		refuse("can't create temporary file");
+	}
+	close(fd);
+	if(save(p, name) < 0){
+		fprint(2, "%s: error saving temporary file: %r\n", argv0);
+		refuse("can't write temporary file");
+	}
+	if(p->tmpbuf){
+		fprint(2, "%s: error in savetmp: already have tmp file!\n",
+			argv0);
+		refuse("already have temporary file");
+	}
+	p->tmpbuf = Bopen(name, OREAD|ORCLOSE);
+	if(p->tmpbuf == nil){
+		fprint(2, "%s: error reading temporary file: %r\n", argv0);
+		refuse("error reading temporary file");
+	}
+	Bseek(p->tmpbuf, bodyoff, 0);
+	return name;
+}
+
+/*
+ * Run the external checker to do content-based checks.
+ */
+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) {
+		free(name);
+		name = strdup(s_to_c(p->filename));
+	}
+	if(strstr(w->msg, "discard")){
+		syslog(0, "mail", "vf validateattachment rejected %s", name);
+		refuse("rejected by validateattachment");
+	}
+	if(strstr(w->msg, "accept")){
+		syslog(0, "mail", "vf validateattachment accepted %s", name);
+		return 1;
+	}
+	free(w);
+	free(name);
+	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;
+
+	if(justreject)
+		return p;
+
+	syslog(0, "mail", "vf wrapped %s %s", p->type?s_to_c(p->type):"?",
+		p->filename?s_to_c(p->filename):"?");
+
+	boundary = mkboundary();
+	/* 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));
+
+	/* 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;
+	}
+
+	/* pass the body */
+	np = passbody(p, 0);
+
+	/* 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));
+	}
+
+	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("/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;
+			}
+		}
+	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;
+
+	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);
+		if(len == -1)
+			goto err;
+		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;
+}