imap-based new upas/fs
diff --git a/src/cmd/upas/nfs/box.c b/src/cmd/upas/nfs/box.c
new file mode 100644
index 0000000..044edf2
--- /dev/null
+++ b/src/cmd/upas/nfs/box.c
@@ -0,0 +1,318 @@
+#include "a.h"
+
+enum
+{
+	BoxSubChunk = 16,
+	BoxChunk = 64,
+	MsgChunk = 256,
+	PartChunk = 4,
+	PartSubChunk = 4,
+};
+
+Box **boxes;
+uint nboxes;
+Box *rootbox;
+int boxid;
+
+Box*
+boxbyname(char *name)
+{
+	int i;
+
+	/* LATER: replace with hash table */
+	for(i=0; i<nboxes; i++)
+		if(boxes[i] && strcmp(boxes[i]->name, name) == 0)
+			return boxes[i];
+	return nil;
+}
+
+Box*
+subbox(Box *b, char *elem)
+{
+	int i;
+	
+	for(i=0; i<b->nsub; i++)
+		if(b->sub[i] && strcmp(b->sub[i]->elem, elem) == 0)
+			return b->sub[i];
+	return nil;
+}
+
+Box*
+boxbyid(uint id)
+{
+	int i;
+	
+	/* LATER: replace with binary search */
+	for(i=0; i<nboxes; i++)
+		if(boxes[i] && boxes[i]->id == id)
+			return boxes[i];
+	return nil;
+}
+
+Box*
+boxcreate(char *name)
+{
+	char *p;
+	Box *b, *bb;
+	
+	if((b = boxbyname(name)) != nil)
+		return b;
+	
+	b = emalloc(sizeof *b);
+	b->id = ++boxid;
+	b->time = time(0);
+	b->name = estrdup(name);
+	b->uidnext = 1;
+	p = strrchr(b->name, '/');
+	if(p){
+		*p = 0;
+		bb = boxcreate(b->name);
+		*p = '/';
+		b->elem = p+1;
+	}else{
+		bb = rootbox;
+		b->elem = b->name;
+	}
+	if(nboxes%BoxChunk == 0)
+		boxes = erealloc(boxes, (nboxes+BoxChunk)*sizeof boxes[0]);
+	boxes[nboxes++] = b;
+	if(bb->nsub%BoxSubChunk == 0)
+		bb->sub = erealloc(bb->sub, (bb->nsub+BoxSubChunk)*sizeof bb->sub[0]);
+	bb->sub[bb->nsub++] = b;
+	b->parent = bb;
+	return b;
+}
+
+void
+boxfree(Box *b)
+{
+	int i;
+	
+	if(b == nil)
+		return;
+	for(i=0; i<b->nmsg; i++)
+		msgfree(b->msg[i]);
+	free(b->msg);
+	free(b);
+}
+
+Part*
+partcreate(Msg *m, Part *pp)
+{
+	Part *p;
+	
+	if(m->npart%PartChunk == 0)
+		m->part = erealloc(m->part, (m->npart+PartChunk)*sizeof m->part[0]);
+	p = emalloc(sizeof *p);
+	p->msg = m;
+	p->ix = m->npart;	
+	m->part[m->npart++] = p;
+	if(pp){
+		if(pp->nsub%PartSubChunk == 0)
+			pp->sub = erealloc(pp->sub, (pp->nsub+PartSubChunk)*sizeof pp->sub[0]);
+		p->pix = pp->nsub;
+		p->parent = pp;
+		pp->sub[pp->nsub++] = p;
+	}
+	return p;
+}
+
+void
+partfree(Part *p)
+{
+	int i;
+	
+	if(p == nil)
+		return;
+	for(i=0; i<p->nsub; i++)
+		partfree(p->sub[i]);
+	free(p->sub);
+	hdrfree(p->hdr);
+	free(p->type);
+	free(p->idstr);
+	free(p->desc);
+	free(p->encoding);
+	free(p->charset);
+	free(p->raw);
+	free(p->rawheader);
+	free(p->rawbody);
+	free(p->mimeheader);
+	free(p->body);
+	free(p);
+}
+
+void
+msgfree(Msg *m)
+{
+	int i;
+	
+	if(m == nil)
+		return;
+	for(i=0; i<m->npart; i++)
+		free(m->part[i]);
+	free(m->part);
+	free(m);
+}
+
+void
+msgplumb(Msg *m, int delete)
+{
+	static int fd = -1;
+	Plumbmsg p;
+	Plumbattr a[10];
+	char buf[256], date[40];
+	int ai;
+	
+	if(m == nil || m->npart < 1 || m->part[0]->hdr == nil)
+		return;
+	if(m->box && strcmp(m->box->name, "mbox") != 0)
+		return;
+
+	p.src = "mailfs";
+	p.dst = "seemail";
+	p.wdir = "/";
+	p.type = "text";
+
+	ai = 0;
+	a[ai].name = "filetype";
+	a[ai].value = "mail";
+	
+	a[++ai].name = "mailtype";
+	a[ai].value = delete?"delete":"new";
+	a[ai-1].next = &a[ai];
+
+	if(m->part[0]->hdr->from){
+		a[++ai].name = "sender";
+		a[ai].value = m->part[0]->hdr->from;
+		a[ai-1].next = &a[ai];
+	}
+
+	if(m->part[0]->hdr->subject){
+		a[++ai].name = "subject";
+		a[ai].value = m->part[0]->hdr->subject;
+		a[ai-1].next = &a[ai];
+	}
+
+	if(m->part[0]->hdr->digest){
+		a[++ai].name = "digest";
+		a[ai].value = m->part[0]->hdr->digest;
+		a[ai-1].next = &a[ai];
+	}
+	
+	strcpy(date, ctime(m->date));
+	date[strlen(date)-1] = 0;	/* newline */
+	a[++ai].name = "date";
+	a[ai].value = date;
+	a[ai-1].next = &a[ai];
+	
+	a[ai].next = nil;
+	
+	p.attr = a;
+	snprint(buf, sizeof buf, "Mail/%s/%ud", m->box->name, m->id);
+	p.ndata = strlen(buf);
+	p.data = buf;
+	
+	if(fd < 0)
+		fd = plumbopen("send", OWRITE);
+	if(fd < 0)
+		return;
+
+	plumbsend(fd, &p);
+}
+
+
+Msg*
+msgcreate(Box *box)
+{
+	Msg *m;
+	
+	m = emalloc(sizeof *m);
+	m->box = box;
+	partcreate(m, nil);
+	m->part[0]->type = estrdup("message/rfc822");
+	if(box->nmsg%MsgChunk == 0)
+		box->msg = erealloc(box->msg, (box->nmsg+MsgChunk)*sizeof box->msg[0]);
+	m->ix = box->nmsg++;
+	box->msg[m->ix] = m;
+	m->id = ++box->msgid;
+	return m;
+}
+
+Msg*
+msgbyimapuid(Box *box, uint uid, int docreate)
+{
+	int i;
+	Msg *msg;
+
+	if(box == nil)
+		return nil;
+	/* LATER: binary search or something */
+	for(i=0; i<box->nmsg; i++)
+		if(box->msg[i]->imapuid == uid)
+			return box->msg[i];
+	if(!docreate)
+		return nil;
+	msg = msgcreate(box);
+	msg->imapuid = uid;
+	return msg;
+}
+
+Msg*
+msgbyid(Box *box, uint id)
+{
+	int i;
+
+	if(box == nil)
+		return nil;
+	/* LATER: binary search or something */
+	for(i=0; i<box->nmsg; i++)
+		if(box->msg[i]->id == id)
+			return box->msg[i];
+	return nil;
+}
+
+Part*
+partbyid(Msg *m, uint id)
+{
+	if(m == nil)
+		return nil;
+	if(id >= m->npart)
+		return nil;
+	return m->part[id];
+}
+
+Part*
+subpart(Part *p, uint a)
+{
+	if(p == nil || a >= p->nsub)
+		return nil;
+	return p->sub[a];
+}
+
+void
+hdrfree(Hdr *h)
+{
+	if(h == nil)
+		return;
+	free(h->date);
+	free(h->subject);
+	free(h->from);
+	free(h->sender);
+	free(h->replyto);
+	free(h->to);
+	free(h->cc);
+	free(h->bcc);
+	free(h->inreplyto);
+	free(h->messageid);
+	free(h->digest);
+	free(h);
+}
+
+void
+boxinit(void)
+{
+	rootbox = emalloc(sizeof *rootbox);
+	rootbox->name = estrdup("");
+	rootbox->time = time(0);
+}
+
diff --git a/src/cmd/upas/nfs/box.h b/src/cmd/upas/nfs/box.h
new file mode 100644
index 0000000..2eeb93e
--- /dev/null
+++ b/src/cmd/upas/nfs/box.h
@@ -0,0 +1,133 @@
+enum
+{
+	FlagJunk = 1<<0,
+	FlagNonJunk = 1<<1,
+	FlagReplied = 1<<2,
+	FlagFlagged = 1<<3,
+	FlagDeleted = 1<<4,
+	FlagDraft = 1<<5,
+	FlagSeen = 1<<6,
+	FlagNoInferiors = 1<<7,
+	FlagMarked = 1<<8,
+	FlagNoSelect = 1<<9,
+	FlagUnMarked = 1<<10,
+};
+
+typedef struct Box Box;
+typedef struct Hdr Hdr;
+typedef struct Msg Msg;
+typedef struct Part Part;
+
+struct Box
+{
+	char*	name;		/* name of mailbox */
+	char*	elem;		/* last element in name */
+	uint		ix;			/* index in box[] array */
+	uint		id;			/* id shown in file system */
+	uint		flags;		/* FlagNoInferiors, etc. */
+	uint		time;			/* last update time */
+	uint		msgid;		/* last message id used */
+
+	Msg**	msg;			/* array of messages (can have nils) */
+	uint		nmsg;
+	
+	char*	imapname;	/* name on IMAP server */
+	u32int	validity;		/* IMAP validity number */
+	uint		uidnext;		/* IMAP expected next uid */
+	uint		recent;		/* IMAP first recent message */
+	uint		exists;		/* IMAP last message in box */
+	uint		maxseen;		/* maximum IMAP uid seen */
+	int		mark;
+	uint		imapinit;		/* up-to-date w.r.t. IMAP */
+
+	Box*		parent;		/* in tree */
+	Box**	sub;
+	uint		nsub;
+};
+
+struct Hdr
+{
+	/* LATER: store date as int, reformat for programs */
+	/* order known by fs.c */
+	char*	date;
+	char*	subject;
+	char*	from;
+	char*	sender;
+	char*	replyto;
+	char*	to;
+	char*	cc;
+	char*	bcc;
+	char*	inreplyto;
+	char*	messageid;
+	char*	digest;
+};
+
+struct Msg
+{
+	Box*		box;			/* mailbox containing msg */
+	uint		ix;			/* index in box->msg[] array */
+	uint		id;			/* id shown in file system */
+	uint		imapuid;		/* IMAP uid */
+	uint		imapid;		/* IMAP id */
+	uint		flags;		/* FlagDeleted etc. */
+	uint		date;			/* smtp envelope date */
+	uint		size;
+	
+	Part**	part;			/* message subparts - part[0] is root */
+	uint		npart;
+};
+
+struct Part
+{
+	Msg*	msg;			/* msg containing part */
+	uint		ix;			/* index in msg->part[] */
+	uint		pix;			/* id in parent->sub[] */
+	Part*		parent;		/* parent in structure */
+	Part**	sub;			/* children in structure */
+	uint		nsub;
+
+	/* order known by fs.c */
+	char*	type;	/* e.g., "text/plain" */
+	char*	idstr;
+	char*	desc;
+	char*	encoding;
+	char*	charset;
+	char*	raw;
+	char*	rawheader;
+	char*	rawbody;
+	char*	mimeheader;
+
+	/* order known by fs.c */
+	uint		size;
+	uint		lines;
+
+	char*	body;
+	uint		nbody;
+	Hdr*		hdr;			/* RFC822 envelope for message/rfc822 */
+};
+
+void		boxinit(void);
+Box*		boxbyname(char*);
+Box*		boxbyid(uint);
+Box*		boxcreate(char*);
+void		boxfree(Box*);
+Box*		subbox(Box*, char*);
+Msg*	msgcreate(Box*);
+Part*	partcreate(Msg*, Part*);
+
+void		hdrfree(Hdr*);
+
+Msg*	msgbyid(Box*, uint);
+Msg*	msgbyimapuid(Box*, uint, int);
+void		msgfree(Msg*);
+void		msgplumb(Msg*, int);
+
+Part*		partbyid(Msg*, uint);
+Part*		subpart(Part*, uint);
+void			partfree(Part*);
+
+extern	Box**	boxes;
+extern	uint		nboxes;
+
+extern	Box*		rootbox;
+
diff --git a/src/cmd/upas/nfs/decode.c b/src/cmd/upas/nfs/decode.c
new file mode 100644
index 0000000..0e46dbf
--- /dev/null
+++ b/src/cmd/upas/nfs/decode.c
@@ -0,0 +1,257 @@
+/* Quick and dirty RFC 2047 */
+
+#include "a.h"
+
+static int
+unhex1(char 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 15;
+}
+
+static int
+unhex(char *s)
+{
+	return unhex1(s[0])*16+unhex1(s[1]);
+}
+
+int
+decqp(uchar *out, int lim, char *in, int n)
+{
+	char *p, *ep;
+	uchar *eout, *out0;
+	
+	out0 = out;
+	eout = out+lim;
+	for(p=in, ep=in+n; p<ep && out<eout; ){
+		if(*p == '_'){
+			*out++ = ' ';
+			p++;
+		}
+		else if(*p == '='){
+			if(p+1 >= ep)
+				break;
+			if(*(p+1) == '\n'){
+				p += 2;
+				continue;
+			}
+			if(p+3 > ep)
+				break;
+			*out++ = unhex(p+1);
+			p += 3;
+		}else
+			*out++ = *p++;
+	}
+	return out-out0;
+}
+
+char*
+decode(int kind, char *s, int *len)
+{
+	char *t;
+	int l;
+
+	if(s == nil)
+		return s;
+	switch(kind){
+	case QuotedPrintable:
+		l = strlen(s)+1;
+		t = emalloc(l);
+		l = decqp((uchar*)t, l, s, l-1);
+		*len = l;
+		t[l] = 0;
+		return t;
+	
+	case Base64:
+		l = strlen(s)+1;
+		t = emalloc(l);
+		l = dec64((uchar*)t, l, s, l-1);
+		*len = l;
+		t[l] = 0;
+		return t;
+
+	default:
+		*len = strlen(s);
+		return estrdup(s);
+	}
+}
+
+struct {
+	char *mime;
+	char *tcs;
+} tcstab[] = {
+	"iso-8859-2",		"8859-2",
+	"iso-8859-3",		"8859-3",
+	"iso-8859-4",		"8859-4",
+	"iso-8859-5",		"8859-5",
+	"iso-8859-6",		"8859-6",
+	"iso-8859-7",		"8859-7",
+	"iso-8859-8",		"8859-8",
+	"iso-8859-9",		"8859-9",
+	"iso-8859-10",	"8859-10",
+	"iso-8859-15",	"8859-15",
+	"big5",			"big5",
+	"iso-2022-jp",	"jis-kanji",
+	"windows-1251",	"cp1251",
+	"koi8-r",			"koi8",
+};
+
+char*
+tcs(char *charset, char *s)
+{
+	static char buf[4096];
+	int i, n;
+	int fd[3], p[2], pp[2];
+	uchar *us;
+	char *t, *u;
+	char *argv[4];
+	Rune r;
+	
+	if(s == nil || charset == nil || *s == 0)
+		return s;
+
+	if(cistrcmp(charset, "utf-8") == 0)
+		return s;
+	if(cistrcmp(charset, "iso-8859-1") == 0 || cistrcmp(charset, "us-ascii") == 0){
+latin1:
+		n = 0;
+		for(us=(uchar*)s; *us; us++)
+			n += runelen(*us);
+		n++;
+		t = emalloc(n);
+		for(us=(uchar*)s, u=t; *us; us++){
+			r = *us;
+			u += runetochar(u, &r);
+		}
+		*u = 0;
+		free(s);
+		return t;
+	}
+	for(i=0; i<nelem(tcstab); i++)
+		if(cistrcmp(charset, tcstab[i].mime) == 0)
+			goto tcs;
+	goto latin1;
+
+tcs:
+	argv[0] = "tcs";
+	argv[1] = "-f";
+	argv[2] = charset;
+	argv[3] = nil;
+	
+	if(pipe(p) < 0 || pipe(pp) < 0)
+		sysfatal("pipe: %r");
+	fd[0] = p[0];
+	fd[1] = pp[0];
+	fd[2] = dup(2, -1);
+	if(threadspawnl(fd, "tcs", "tcs", "-f", tcstab[i].tcs, nil) < 0){
+		close(p[0]);
+		close(p[1]);
+		close(pp[0]);
+		close(pp[1]);
+		close(fd[2]);
+		goto latin1;
+	}
+	close(p[0]);
+	close(pp[0]);
+	write(p[1], s, strlen(s));
+	close(p[1]);
+	n = readn(pp[1], buf, sizeof buf-1);
+	close(pp[1]);
+	if(n <= 0)
+		goto latin1;
+	free(s);
+	buf[n] = 0;
+	return estrdup(buf);
+}
+
+char*
+unrfc2047(char *s)
+{
+	char *p, *q, *t, *u, *v;
+	int len;
+	Rune r;
+	Fmt fmt;
+	
+	if(s == nil)
+		return nil;
+
+	if(strstr(s, "=?") == nil)
+		return s;
+	
+	fmtstrinit(&fmt);
+	for(p=s; *p; ){
+		/* =?charset?e?text?= */
+		if(*p=='=' && *(p+1)=='?'){
+			p += 2;
+			q = strchr(p, '?');
+			if(q == nil)
+				goto emit;
+			q++;
+			if(*q == '?' || *(q+1) != '?')
+				goto emit;
+			t = q+2;
+			u = strchr(t, '?');
+			if(u == nil || *(u+1) != '=')
+				goto emit;
+			switch(*q){
+			case 'q':
+			case 'Q':
+				*u = 0;
+				v = decode(QuotedPrintable, t, &len);
+				break;
+			case 'b':
+			case 'B':
+				*u = 0;
+				v = decode(Base64, t, &len);
+				break;
+			default:
+				goto emit;
+			}
+			*(q-1) = 0;
+			v = tcs(p, v);
+			fmtstrcpy(&fmt, v);
+			free(v);
+			p = u+2;
+		}
+	emit:
+		p += chartorune(&r, p);
+		fmtrune(&fmt, r);
+	}
+	p = fmtstrflush(&fmt);
+	if(p == nil)
+		sysfatal("out of memory");
+	free(s);
+	return p;
+}
+
+#ifdef TEST
+char *test[] = 
+{
+	"hello world",
+	"hello =?iso-8859-1?q?this is some text?=",
+	"=?US-ASCII?Q?Keith_Moore?=",
+	"=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?=",
+	"=?ISO-8859-1?Q?Andr=E9?= Pirard",
+	"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=",
+	"=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=",
+	"=?ISO-8859-1?Q?Olle_J=E4rnefors?=",
+	"=?iso-2022-jp?B?GyRCTTVKISRKP006SiRyS34kPyQ3JEZKcz03JCIkahsoQg==?=",
+	"=?UTF-8?B?Ik5pbHMgTy4gU2Vsw6VzZGFsIg==?="
+};
+
+void
+threadmain(int argc, char **argv)
+{
+	int i;
+	
+	for(i=0; i<nelem(test); i++)
+		print("%s\n\t%s\n", test[i], unrfc2047(estrdup(test[i])));
+	threadexitsall(0);
+}
+
+#endif
diff --git a/src/cmd/upas/nfs/fs.c b/src/cmd/upas/nfs/fs.c
new file mode 100644
index 0000000..eda3d66
--- /dev/null
+++ b/src/cmd/upas/nfs/fs.c
@@ -0,0 +1,1259 @@
+/*
+ * Mail file system.
+ * 
+ * Serve the bulk of requests out of memory, so they can
+ * be in the main loop (will never see their flushes). 
+ * Some requests do block and they get handled in
+ * separate threads.  They're all okay to give up on
+ * early, though, so we just respond saying interrupted
+ * and then when they finish, silently discard the request.
+
+TO DO:
+
+	decode subject, etc.
+	decode body
+
+	digest
+	disposition
+	filename
+	
+	ctl messages
+
+	fetch mail on demand
+
+ */
+
+#include "a.h"
+
+enum
+{
+	/* directories */
+	Qroot,
+	Qbox,
+	Qmsg,
+
+	/* control files */
+	Qctl,
+	Qboxctl,
+	Qsearch,
+
+	/* message header - same order as struct Hdr */
+	Qdate,
+	Qsubject,
+	Qfrom,
+	Qsender,
+	Qreplyto,
+	Qto,
+	Qcc,
+	Qbcc,
+	Qinreplyto,
+	Qmessageid,
+	
+	/* part data - same order as stuct Part */
+	Qtype,
+	Qidstr,
+	Qdesc,
+	Qencoding,	/* only here temporarily! */
+	Qcharset,
+	Qraw,
+	Qrawheader,
+	Qrawbody,
+	Qmimeheader,
+	
+	/* part numbers - same order as struct Part */
+	Qsize,
+	Qlines,
+
+	/* other message files */
+	Qbody,
+	Qheader,
+	Qdigest,
+	Qdisposition,
+	Qfilename,
+	Qflags,
+	Qinfo,
+	Qrawunix,
+	Qunixdate,
+	Qunixheader,
+	
+	Qfile0 = Qbody,
+	Qnfile = Qunixheader+1-Qfile0,
+};
+
+static char Egreg[] = "gone postal";
+static char Enobox[] = "no such mailbox";
+static char Enomsg[] = "no such message";
+static char Eboxgone[] = "mailbox not available";
+static char Emsggone[] = "message not available";
+static char Eperm[] = "permission denied";
+static char Ebadctl[] = "bad control message";
+
+Channel *fsreqchan;
+Srv fs;
+Qid rootqid;
+ulong t0;
+
+void
+responderror(Req *r)
+{
+	char e[ERRMAX];
+	
+	rerrstr(e, sizeof e);
+	respond(r, e);
+}
+
+int
+qtype(Qid q)
+{
+	return q.path&0x3F;
+}
+
+int
+qboxid(Qid q)
+{
+	return (q.path>>40)&0xFFFF;
+}
+
+int
+qmsgid(Qid q)
+{
+	return ((q.path>>32)&0xFF000000) | ((q.path>>16)&0xFFFFFF);
+}
+
+int
+qpartid(Qid q)
+{
+	return ((q.path>>6)&0x3FF);
+}
+
+Qid
+qid(int ctl, Box *box, Msg *msg, Part *part)
+{
+	Qid q;
+	
+	q.type = 0;
+	if(ctl == Qroot || ctl == Qbox || ctl == Qmsg)
+		q.type = QTDIR;
+	q.path = (vlong)((msg ? msg->id : 0)&0xFF000000)<<32;
+	q.path |= (vlong)((msg ? msg->id : 0)&0xFFFFFF)<<16;
+	q.path |= (vlong)((box ? box->id : 0)&0xFFFF)<<40;
+	q.path |= ((part ? part->ix : 0)&0x3FF)<<6;
+	q.path |= ctl&0x3F;
+	q.vers = box ? box->validity : 0;
+	return q;
+}
+
+int
+parseqid(Qid q, Box **box, Msg **msg, Part **part)
+{
+	*msg = nil;
+	*part = nil;
+	
+	*box = boxbyid(qboxid(q));
+	if(*box){
+		*msg = msgbyid(*box, qmsgid(q));
+	}
+	if(*msg)
+		*part = partbyid(*msg, qpartid(q));
+	return qtype(q);
+}
+
+static struct {
+	int type;
+	char *name;
+} typenames[] = {
+	Qbody,			"body",
+	Qbcc,			"bcc",
+	Qcc,				"cc",
+	Qdate,			"date",
+	Qfilename,		"filename",
+	Qflags,			"flags",
+	Qfrom,			"from",
+	Qheader,			"header",
+	Qinfo,			"info",
+	Qinreplyto,		"inreplyto",
+	Qlines,			"lines",
+	Qmimeheader,	"mimeheader",
+	Qmessageid,		"messageid",
+	Qraw,			"raw",
+	Qrawunix,		"rawunix",
+	Qrawbody,		"rawbody",
+	Qrawheader,		"rawheader",
+	Qreplyto,			"replyto",
+	Qsender,			"sender",
+	Qsubject,		"subject",
+	Qto,				"to",
+	Qtype,			"type",
+	Qunixdate,		"unixdate",
+	Qunixheader,		"unixheader",
+	Qidstr,			"idstr",
+	Qdesc,			"desc",
+	Qencoding,		"encoding",
+	Qcharset,		"charset",
+};
+
+char*
+nameoftype(int t)
+{
+	int i;
+	
+	for(i=0; i<nelem(typenames); i++)
+		if(typenames[i].type == t)
+			return typenames[i].name;
+	return "???";
+}
+
+int
+typeofname(char *name)
+{
+	int i;
+	
+	for(i=0; i<nelem(typenames); i++)
+		if(strcmp(typenames[i].name, name) == 0)
+			return typenames[i].type;
+	return 0;
+}
+
+static void
+fsattach(Req *r)
+{
+	r->fid->qid = rootqid;
+	r->ofcall.qid = rootqid;
+	respond(r, nil);
+}
+
+int
+isnumber(char *s)
+{
+	int n;
+
+	if(*s < '1' || *s > '9')
+		return 0;
+	n = strtol(s, &s, 10);
+	if(*s != 0)
+		return 0;
+	return n;
+}
+
+static char*
+fswalk1(Fid *fid, char *name, void *arg)
+{
+	int a, type;
+	Box *b, *box;
+	Msg *msg;
+	Part *p, *part;
+
+	USED(arg);
+	
+	switch(type = parseqid(fid->qid, &box, &msg, &part)){
+	case Qroot:
+		if(strcmp(name, "..") == 0)
+			return nil;
+		if(strcmp(name, "ctl") == 0){
+			fid->qid = qid(Qctl, nil, nil, nil);
+			return nil;
+		}
+		if((box = boxbyname(name)) != nil){
+			fid->qid = qid(Qbox, box, nil, nil);
+			return nil;
+		}
+		break;
+
+	case Qbox:
+		/*
+		 * Would be nice if .. could work even if the box is gone,
+		 * but we don't know how deep the directory was.
+		 */
+		if(box == nil)
+			return Eboxgone;
+		if(strcmp(name, "..") == 0){
+			if((box = box->parent) == nil){
+				fid->qid = rootqid;
+				return nil;
+			}
+			fid->qid = qid(Qbox, box, nil, nil);
+			return nil;
+		}
+		if(strcmp(name, "ctl") == 0){
+			fid->qid = qid(Qboxctl, box, nil, nil);
+			return nil;
+		}
+		if(strcmp(name, "search") == 0){
+			fid->qid = qid(Qsearch, box, nil, nil);
+			return nil;
+		}
+		if((b = subbox(box, name)) != nil){
+			fid->qid = qid(Qbox, b, nil, nil);
+			return nil;
+		}
+		if((a = isnumber(name)) != 0){
+			if((msg = msgbyid(box, a)) == nil){
+				return Enomsg;
+			}
+			fid->qid = qid(Qmsg, box, msg, nil);
+			return nil;
+		}
+		break;
+
+	case Qmsg:
+		if(strcmp(name, "..") == 0){
+			if(part == msg->part[0]){
+				fid->qid = qid(Qbox, box, nil, nil);
+				return nil;
+			}
+			fid->qid = qid(Qmsg, box, msg, part->parent);
+			return nil;
+		}
+		if((type = typeofname(name)) > 0){
+			/* XXX - should check that type makes sense (see msggen) */
+			fid->qid = qid(type, box, msg, part);
+			return nil;
+		}
+		if((a = isnumber(name)) != 0){
+			if((p = subpart(part, a-1)) != nil){
+				fid->qid = qid(Qmsg, box, msg, p);
+				return nil;
+			}
+		}
+		break;
+	}
+	return "not found";
+}
+
+static void
+fswalk(Req *r)
+{
+	walkandclone(r, fswalk1, nil, nil);
+}
+
+static struct {
+	int flag;
+	char *name;
+} flagtab[] = {
+	FlagJunk,			"junk",
+	FlagNonJunk,		"notjunk",
+	FlagReplied,	"replied",
+	FlagFlagged,		"flagged",
+//	FlagDeleted,		"deleted",
+	FlagDraft,		"draft",
+	FlagSeen,			"seen",
+};
+
+static void
+addaddrs(Fmt *fmt, char *prefix, char *addrs)
+{
+	char **f;
+	int i, nf, inquote;
+	char *p, *sep;
+	
+	if(addrs == nil)
+		return;
+	addrs = estrdup(addrs);
+	nf = 0;
+	inquote = 0;
+	for(p=addrs; *p; p++){
+		if(*p == ' ' && !inquote)
+			nf++;
+		if(*p == '\'')
+			inquote = !inquote;
+	}
+	nf += 10;
+	f = emalloc(nf*sizeof f[0]);
+	nf = tokenize(addrs, f, nf);
+	fmtprint(fmt, "%s:", prefix);
+	sep = " ";
+	for(i=0; i+1<nf; i+=2){
+		if(f[i][0])
+			fmtprint(fmt, "%s%s <%s>", sep, f[i], f[i+1]);
+		else
+			fmtprint(fmt, "%s%s", sep, f[i+1]);
+		sep = ", ";
+	}
+	fmtprint(fmt, "\n");
+	free(addrs);
+}
+
+static void
+mkbody(Part *p, Qid q)
+{
+	char *t;
+	int len;
+	
+	if(p->msg->part[0] == p)
+		t = p->rawbody;
+	else
+		t = p->raw;
+	if(t == nil)
+		return;
+
+	len = -1;
+	if(p->encoding && cistrcmp(p->encoding, "quoted-printable") == 0)
+		t = decode(QuotedPrintable, t, &len);
+	else if(p->encoding && cistrcmp(p->encoding, "base64") == 0)
+		t = decode(Base64, t, &len);
+	else
+		t = estrdup(t);
+
+	if(p->charset){
+		t = tcs(p->charset, t);
+		len = -1;
+	}
+	p->body = t;
+	if(len == -1)
+		p->nbody = strlen(t);
+	else
+		p->nbody = len;
+}
+
+static Qid ZQ;
+	
+static int
+filedata(int type, Box *box, Msg *msg, Part *part, char **pp, int *len, int *freeme, int force, Qid q)
+{
+	int i, inquote, n, t;
+	char *from, *s;
+	static char buf[256];
+	Fmt fmt;
+
+	*pp = nil;
+	*freeme = 0;
+	if(len)
+		*len = -1;
+
+	if(msg == nil || part == nil){
+		werrstr(Emsggone);
+		return -1;
+	}
+	switch(type){
+	case Qdate:
+	case Qsubject:
+	case Qfrom:
+	case Qsender:
+	case Qreplyto:
+	case Qto:
+	case Qcc:
+	case Qbcc:
+	case Qinreplyto:
+	case Qmessageid:
+		if(part->hdr == nil){
+			werrstr(Emsggone);
+			return -1;
+		}
+		*pp = ((char**)&part->hdr->date)[type-Qdate];
+		return 0;
+	
+	case Qunixdate:
+		strcpy(buf, ctime(msg->date));
+		*pp = buf;
+		return 0;
+
+	case Qunixheader:
+		if(part->hdr == nil){
+			werrstr(Emsggone);
+			return -1;
+		}
+		from = part->hdr->from;
+		if(from == nil)
+			from = "???";
+		else{
+			inquote = 0;
+			for(; *from; from++){
+				if(*from == '\'')
+					inquote = !inquote;
+				if(!inquote && *from == ' '){
+					from++;
+					break;
+				}
+			}
+			if(*from == 0)
+				from = part->hdr->from;
+		}
+		n = snprint(buf, sizeof buf, "From %s %s", from, ctime(msg->date));
+		if(n+1 < sizeof buf){
+			*pp = buf;
+			return 0;
+		}
+		fmtstrinit(&fmt);
+		fmtprint(&fmt, "From %s %s", from, ctime(msg->date));
+		s = fmtstrflush(&fmt);
+		if(s){
+			*pp = s;
+			*freeme = 1;
+		}else
+			*pp = buf;
+		return 0;
+
+	case Qtype:
+	case Qidstr:
+	case Qdesc:
+	case Qencoding:
+	case Qcharset:
+	case Qraw:
+	case Qrawheader:
+	case Qrawbody:
+	case Qmimeheader:
+		*pp = ((char**)&part->type)[type-Qtype];
+		if(*pp == nil && force){
+			switch(type){
+			case Qraw:
+				imapfetchraw(imap, part);
+				break;
+			case Qrawheader:
+				imapfetchrawheader(imap, part);
+				break;
+			case Qrawbody:
+				imapfetchrawbody(imap, part);
+				break;
+			case Qmimeheader:
+				imapfetchrawmime(imap, part);
+				break;
+			default:
+				return 0;
+			}
+			/*
+			 * We ran fetchsomething, which might have changed
+			 * the mailbox contents.  Msg might even be gone.
+			 */
+			t = parseqid(q, &box, &msg, &part);
+			if(t != type || msg == nil || part == nil)
+				return 0;
+			*pp = ((char**)&part->type)[type-Qtype];
+		}
+		return 0;
+
+	case Qbody:
+		if(part->body){
+			*pp = part->body;
+			if(len)
+				*len = part->nbody;
+			return 0;
+		}
+		if(!force)
+			return 0;
+		if(part->rawbody == nil){
+			if(part->msg->part[0] == part)
+				imapfetchrawbody(imap, part);
+			else
+				imapfetchraw(imap, part);
+			t = parseqid(q, &box, &msg, &part);
+			if(t != type || msg == nil || part == nil)
+				return 0;
+		}
+		mkbody(part, q);
+		*pp = part->body;
+		if(len)
+			*len = part->nbody;
+		return 0;
+
+	case Qsize:
+	case Qlines:
+		n = ((uint*)&part->size)[type-Qsize];
+		snprint(buf, sizeof buf, "%d", n);
+		*pp = buf;
+		return 0;
+
+	case Qflags:
+		s = buf;
+		*s = 0;
+		for(i=0; i<nelem(flagtab); i++){
+			if(msg->flags&flagtab[i].flag){
+				if(s > buf)
+					*s++ = ' ';
+				strcpy(s, flagtab[i].name);
+				s += strlen(s);
+			}
+		}
+		*pp = buf;
+		return 0;
+
+	case Qinfo:
+		fmtstrinit(&fmt);
+		if(part == msg->part[0]){
+			if(msg->date)
+				fmtprint(&fmt, "unixdate %lud %s", msg->date, ctime(msg->date));
+			if(msg->flags){
+				filedata(Qflags, box, msg, part, pp, nil, freeme, 0, ZQ);
+				fmtprint(&fmt, "flags %s\n", buf);
+			}
+		}
+		if(part->hdr){
+			if(part->hdr->digest)
+				fmtprint(&fmt, "digest %s\n", part->hdr->digest);
+			if(part->hdr->from)
+				fmtprint(&fmt, "from %s\n", part->hdr->from);
+			if(part->hdr->to)
+				fmtprint(&fmt, "to %s\n", part->hdr->to);
+			if(part->hdr->cc)
+				fmtprint(&fmt, "cc %s\n", part->hdr->cc);
+			if(part->hdr->replyto)
+				fmtprint(&fmt, "replyto %s\n", part->hdr->replyto);
+			if(part->hdr->bcc)
+				fmtprint(&fmt, "bcc %s\n", part->hdr->bcc);
+			if(part->hdr->inreplyto)
+				fmtprint(&fmt, "inreplyto %s\n", part->hdr->inreplyto);
+			if(part->hdr->date)
+				fmtprint(&fmt, "date %s\n", part->hdr->date);
+			if(part->hdr->sender)
+				fmtprint(&fmt, "sender %s\n", part->hdr->sender);
+			if(part->hdr->messageid)
+				fmtprint(&fmt, "messageid %s\n", part->hdr->messageid);
+			if(part->hdr->subject)
+				fmtprint(&fmt, "subject %s\n", part->hdr->subject);
+		}
+		if(part->type)
+			fmtprint(&fmt, "type %s\n", part->type);
+		if(part->lines)
+			fmtprint(&fmt, "lines %d\n", part->lines);
+	//	fmtprint(&fmt, "disposition %s\n", "" /* disposition */);
+	//	fmtprint(&fmt, "filename %s\n", "" /* filename */);
+	//	fmtprint(&fmt, "digest %s\n", "" /* digest */);
+		s = fmtstrflush(&fmt);
+		if(s == nil)
+			s = estrdup("");
+		*freeme = 1;
+		*pp = s;
+		return 0;
+
+	case Qheader:
+		if(part->hdr == nil)
+			return 0;
+		fmtstrinit(&fmt);
+		if(part == msg->part[0])
+			fmtprint(&fmt, "Date: %s", ctime(msg->date));
+		else
+			fmtprint(&fmt, "Date: %s\n", part->hdr->date);
+		addaddrs(&fmt, "To", part->hdr->to);
+		addaddrs(&fmt, "From", part->hdr->from);
+		if(part->hdr->from==nil
+		|| (part->hdr->sender && strcmp(part->hdr->sender, part->hdr->from) != 0))
+			addaddrs(&fmt, "Sender", part->hdr->sender);
+		if(part->hdr->from==nil 
+		|| (part->hdr->replyto && strcmp(part->hdr->replyto, part->hdr->from) != 0))
+			addaddrs(&fmt, "Reply-To", part->hdr->replyto);
+		addaddrs(&fmt, "Subject", part->hdr->subject);
+		s = fmtstrflush(&fmt);
+		if(s == nil)
+			s = estrdup("");
+		*freeme = 1;
+		*pp = s;
+		return 0;
+
+	default:
+		werrstr(Egreg);
+		return -1;
+	}
+}
+
+int
+filldir(Dir *d, int type, Box *box, Msg *msg, Part *part)
+{
+	int freeme, len;
+	char *s;
+
+	memset(d, 0, sizeof *d);
+	if(box){
+		d->atime = box->time;
+		d->mtime = box->time;
+	}else{
+		d->atime = t0;
+		d->mtime = t0;
+	}
+	d->uid = estrdup9p("upas");
+	d->gid = estrdup9p("upas");
+	d->muid = estrdup9p("upas");
+	d->qid = qid(type, box, msg, part);
+
+	switch(type){
+	case Qroot:
+	case Qbox:
+	case Qmsg:
+		d->mode = 0555|DMDIR;
+		if(box && !(box->flags&FlagNoInferiors))
+			d->mode = 0775|DMDIR;
+		break;
+	case Qctl:
+	case Qboxctl:
+		d->mode = 0222;
+		break;
+	case Qsearch:
+		d->mode = 0666;
+		break;
+	
+	case Qflags:
+		d->mode = 0666;
+		goto msgfile;
+	default:
+		d->mode = 0444;
+	msgfile:
+		if(filedata(type, box, msg, part, &s, &len, &freeme, 0, ZQ) >= 0){
+			if(s){
+				if(len == -1)
+					d->length = strlen(s);
+				else
+					d->length = len;
+				if(freeme)
+					free(s);
+			}
+		}else if(type == Qraw && msg && part == msg->part[0])
+			d->length = msg->size;
+		break;
+	}
+
+	switch(type){
+	case Qroot:
+		d->name = estrdup9p("/");
+		break;
+	case Qbox:
+		if(box == nil){
+			werrstr(Enobox);
+			return -1;
+		}
+		d->name = estrdup9p(box->elem);
+		break;
+	case Qmsg:
+		if(msg == nil){
+			werrstr(Enomsg);
+			return -1;
+		}
+		if(part == nil || part == msg->part[0])
+			d->name = esmprint("%d", msg->id);
+		else
+			d->name = esmprint("%d", part->pix+1);
+		break;
+	case Qctl:
+	case Qboxctl:
+		d->name = estrdup9p("ctl");
+		break;
+	case Qsearch:
+		d->name = estrdup9p("search");
+		break;
+	default:
+		d->name = estrdup9p(nameoftype(type));
+		break;
+	}
+	return 0;
+}
+	
+static void
+fsstat(Req *r)
+{
+	int type;
+	Box *box;
+	Msg *msg;
+	Part *part;
+	
+	type = parseqid(r->fid->qid, &box, &msg, &part);
+	if(filldir(&r->d, type, box, msg, part) < 0)
+		responderror(r);
+	else
+		respond(r, nil);
+}
+
+int
+rootgen(int i, Dir *d, void *aux)
+{
+	USED(aux);
+	
+	if(i == 0)
+		return filldir(d, Qctl, nil, nil, nil);
+	i--;
+	if(i < rootbox->nsub)
+		return filldir(d, Qbox, rootbox->sub[i], nil, nil);
+	return -1;
+}
+
+int
+boxgen(int i, Dir *d, void *aux)
+{
+	Box *box;
+
+	box = aux;
+if(i==0) fprint(2, "boxgen %s %d nsub=%d nmsg=%d\n", box->name, i, box->nsub, box->nmsg);
+	if(i == 0)
+		return filldir(d, Qboxctl, box, nil, nil);
+	i--;
+	if(i == 0)
+		return filldir(d, Qsearch, box, nil, nil);
+	if(i < box->nsub)
+		return filldir(d, Qbox, box->sub[i], nil, nil);
+	i -= box->nsub;
+	if(i < box->nmsg)
+		return filldir(d, Qmsg, box, box->msg[i], nil);
+	return -1;
+}
+
+static int msgdir[] = {
+	Qtype, 
+	Qbody, Qbcc, Qcc, Qdate, Qflags, Qfrom, Qheader, Qinfo, 
+	Qinreplyto, Qlines, Qmimeheader, Qmessageid, 
+	Qraw, Qrawunix, Qrawbody, Qrawheader,
+	Qreplyto, Qsender, Qsubject, Qto,
+	Qunixdate, Qunixheader
+};
+static int mimemsgdir[] = {
+	Qtype, 
+	Qbody, Qbcc, Qcc, Qdate, Qfrom, Qheader, Qinfo, 
+	Qinreplyto, Qlines, Qmimeheader, Qmessageid, 
+	Qraw, Qrawunix, Qrawbody, Qrawheader,
+	Qreplyto, Qsender, Qsubject, Qto,
+};
+static int mimedir[] = {
+	Qtype,
+	Qbody,
+	Qmimeheader,
+	Qraw,
+};
+	
+int
+msggen(int i, Dir *d, void *aux)
+{
+	Box *box;
+	Msg *msg;
+	Part *part;
+	
+	part = aux;
+	msg = part->msg;
+	box = msg->box;
+	if(part->ix == 0){
+		if(i < nelem(msgdir))
+			return filldir(d, msgdir[i], box, msg, part);
+		i -= nelem(msgdir);
+	}else if(part->type && strcmp(part->type, "message/rfc822") == 0){
+		if(i < nelem(mimemsgdir))
+			return filldir(d, mimemsgdir[i], box, msg, part);
+		i -= nelem(mimemsgdir);
+	}else{
+		if(i < nelem(mimedir))
+			return filldir(d, mimedir[i], box, msg, part);
+		i -= nelem(mimedir);
+	}
+	if(i < part->nsub)
+		return filldir(d, Qmsg, box, msg, part->sub[i]);
+	return -1;
+}
+
+enum
+{
+	CMhangup,
+};
+static Cmdtab ctltab[] =
+{
+	CMhangup, "hangup", 2,
+};
+
+enum
+{
+	CMdelete,
+	CMrefresh,
+	CMreplied,
+	CMread,
+	CMsave,
+	CMjunk,
+	CMnonjunk,
+};
+static Cmdtab boxctltab[] =
+{
+	CMdelete,	"delete",	0,
+	CMrefresh,	"refresh", 1,
+	CMreplied,	"replied", 0,
+	CMread,		"read", 0,
+	CMsave,		"save", 0,
+	CMjunk,		"junk", 0,
+	CMnonjunk,	"nonjunk", 0,
+};
+
+static void
+fsread(Req *r)
+{
+	char *s;
+	int type, len, freeme;
+	Box *box;
+	Msg *msg;
+	Part *part;
+	
+	switch(type = parseqid(r->fid->qid, &box, &msg, &part)){
+	case Qroot:
+		dirread9p(r, rootgen, nil);
+		respond(r, nil);
+		return;
+
+	case Qbox:
+		if(box == nil){
+			respond(r, Eboxgone);
+			return;
+		}
+		if(box->nmsg == 0)
+			imapcheckbox(imap, box);
+		parseqid(r->fid->qid, &box, &msg, &part);
+		if(box == nil){
+			respond(r, Eboxgone);
+			return;
+		}
+		dirread9p(r, boxgen, box);
+		respond(r, nil);
+		return;
+
+	case Qmsg:
+		if(msg == nil || part == nil){
+			respond(r, Emsggone);
+			return;
+		}
+		dirread9p(r, msggen, part);
+		respond(r, nil);
+		return;
+	
+	case Qctl:
+	case Qboxctl:
+		respond(r, Egreg);
+		return;
+
+	case Qsearch:
+		readstr(r, r->fid->aux);
+		respond(r, nil);
+		return;
+
+	default:
+		if(filedata(type, box, msg, part, &s, &len, &freeme, 1, r->fid->qid) < 0){
+			responderror(r);
+			return;
+		}
+		if(s && len == -1)
+			len = strlen(s);
+		readbuf(r, s, len);
+		if(freeme)
+			free(s);
+		respond(r, nil);
+		return;
+	}
+}
+
+int
+mkmsglist(Box *box, char **f, int nf, Msg ***mm)
+{
+	int i, nm;
+	Msg **m;
+	
+	m = emalloc(nf*sizeof m[0]);
+	nm = 0;
+	for(i=0; i<nf; i++)
+		if((m[nm] = msgbyid(box, atoi(f[i]))) != nil)
+			nm++;
+	*mm = m;
+	return nm;
+}
+
+static void
+fswrite(Req *r)
+{
+	int i, j, c, type, flag, unflag, flagset, f, reset;
+	Box *box;
+	Msg *msg;
+	Part *part;
+	Cmdbuf *cb;
+	Cmdtab *ct;
+	Msg **m;
+	int nm;
+	Fmt fmt;
+
+	r->ofcall.count = r->ifcall.count;
+	switch(type = parseqid(r->fid->qid, &box, &msg, &part)){
+	default:
+		respond(r, Egreg);
+		break;
+
+	case Qctl:
+		cb = parsecmd(r->ifcall.data, r->ifcall.count);
+		if((ct = lookupcmd(cb, ctltab, nelem(ctltab))) == nil){
+			respondcmderror(r, cb, "unknown message");
+			free(cb);
+			return;
+		}
+		r->ofcall.count = r->ifcall.count;
+		switch(ct->index){
+		case CMhangup:
+			imaphangup(imap, atoi(cb->f[1]));
+			respond(r, nil);
+			break;
+		default:
+			respond(r, Egreg);
+			break;
+		}
+		free(cb);
+		return;
+
+	case Qboxctl:
+		cb = parsecmd(r->ifcall.data, r->ifcall.count);
+		if((ct = lookupcmd(cb, boxctltab, nelem(boxctltab))) == nil){
+			respondcmderror(r, cb, "bad message");
+			free(cb);
+			return;
+		}
+		r->ofcall.count = r->ifcall.count;
+		switch(ct->index){
+		case CMsave:
+			if(cb->nf <= 2){
+				respondcmderror(r, cb, Ebadctl);
+				break;
+			}
+			nm = mkmsglist(box, cb->f+2, cb->nf-2, &m);
+			if(nm != cb->nf-2){
+			//	free(m);
+				respond(r, Enomsg);
+				break;
+			}
+			if(nm > 0 && imapcopylist(imap, cb->f[1], m, nm) < 0)
+				responderror(r);
+			else
+				respond(r, nil);
+			free(m);
+			break;
+		
+		case CMjunk:
+			flag = FlagJunk;
+			goto flagit;
+		case CMnonjunk:
+			flag = FlagNonJunk;
+			goto flagit;
+		case CMreplied:
+			flag = FlagReplied;
+			goto flagit;
+		case CMread:
+			flag = FlagSeen;
+		flagit:
+			if(cb->nf <= 1){
+				respondcmderror(r, cb, Ebadctl);
+				break;
+			}
+			nm = mkmsglist(box, cb->f+1, cb->nf-1, &m);
+			if(nm != cb->nf-1){
+				free(m);
+				respond(r, Enomsg);
+				break;
+			}
+			if(nm > 0 && imapflaglist(imap, +1, flag, m, nm) < 0)
+				responderror(r);
+			else
+				respond(r, nil);
+			free(m);
+			break;
+
+		case CMrefresh:
+			imapcheckbox(imap, box);
+			respond(r, nil);
+			break;
+
+		case CMdelete:
+			if(cb->nf <= 1){
+				respondcmderror(r, cb, Ebadctl);
+				break;
+			}
+			nm = mkmsglist(box, cb->f+1, cb->nf-1, &m);
+			if(nm > 0 && imapremovelist(imap, m, nm) < 0)
+				responderror(r);
+			else
+				respond(r, nil);
+			free(m);
+			break;
+
+		default:
+			respond(r, Egreg);
+			break;
+		}
+		free(cb);
+		return;
+
+	case Qflags:
+		if(msg == nil){
+			respond(r, Enomsg);
+			return;
+		}
+		cb = parsecmd(r->ifcall.data, r->ifcall.count);
+		flag = 0;
+		unflag = 0;
+		flagset = 0;
+		reset = 0;
+		for(i=0; i<cb->nf; i++){
+			f = 0;
+			c = cb->f[i][0];
+			if(c == '+' || c == '-')
+				cb->f[i]++;
+			for(j=0; j<nelem(flagtab); j++){
+				if(strcmp(flagtab[j].name, cb->f[i]) == 0){
+					f = flagtab[j].flag;
+					break;
+				}
+			}
+			if(f == 0){
+				respondcmderror(r, cb, "unknown flag %s", cb->f[i]);
+				free(cb);
+				return;
+			}
+			if(c == '+')
+				flag |= f;
+			else if(c == '-')
+				unflag |= f;
+			else
+				flagset |= f;
+		}
+		free(cb);
+		if((flagset!=0)+(unflag!=0)+(flag!=0) != 1){
+			respondcmderror(r, cb, Ebadctl);
+			return;
+		}
+		if(flag)
+			i = 1;
+		else if(unflag){
+			i = -1;
+			flag = unflag;
+		}else{
+			i = 0;
+			flag = flagset;
+		}
+		if(imapflaglist(imap, i, flag, &msg, 1) < 0)
+			responderror(r);
+		else
+			respond(r, nil);
+		return;
+
+	case Qsearch:
+		if(box == nil){
+			respond(r, Eboxgone);
+			return;
+		}
+		fmtstrinit(&fmt);
+		nm = imapsearchbox(imap, box, r->ifcall.data, &m);
+		for(i=0; i<nm; i++){
+			if(i>0)
+				fmtrune(&fmt, ' ');
+			fmtprint(&fmt, "%d", m[i]->id);
+		}
+		free(r->fid->aux);
+		r->fid->aux = fmtstrflush(&fmt);
+		respond(r, nil);
+		return;
+	}
+}
+
+static void
+fsopen(Req *r)
+{
+	switch(qtype(r->fid->qid)){
+	case Qctl:
+	case Qboxctl:
+		if((r->ifcall.mode&~OTRUNC) != OWRITE){
+			respond(r, Eperm);
+			return;
+		}
+		respond(r, nil);
+		return;
+
+	case Qflags:
+	case Qsearch:
+		if((r->ifcall.mode&~OTRUNC) > ORDWR){
+			respond(r, Eperm);
+			return;
+		}
+		respond(r, nil);
+		return;
+
+	default:
+		if(r->ifcall.mode != OREAD){
+			respond(r, Eperm);
+			return;
+		}
+		respond(r, nil);
+		return;
+	}
+}
+
+static void
+fsflush(Req *r)
+{
+	/*
+	 * We only handle reads and writes outside the main loop,
+	 * so we must be flushing one of those.  In both cases it's
+	 * okay to just ignore the results of the request, whenever
+	 * they're ready.
+	 */
+	incref(&r->oldreq->ref);
+	respond(r->oldreq, "interrupted");
+	respond(r, nil);
+}
+
+static void
+fsthread(void *v)
+{
+	Req *r;
+	
+	r = v;
+	switch(r->ifcall.type){
+	case Tread:
+		fsread(r);
+		break;
+	case Twrite:
+		fswrite(r);
+		break;
+	}
+}
+
+static void
+fsrecv(void *v)
+{
+	Req *r;
+	
+	while((r = recvp(fsreqchan)) != nil){
+		switch(r->ifcall.type){
+		case Tattach:
+			fsattach(r);
+			break;
+		case Tflush:
+			fsflush(r);
+			break;
+		case Topen:
+			fsopen(r);
+			break;
+		case Twalk:
+			fswalk(r);
+			break;
+		case Tstat:
+			fsstat(r);
+			break;
+		default:
+			threadcreate(fsthread, r, STACK);
+			break;
+		}
+	}
+}
+
+static void
+fssend(Req *r)
+{
+	sendp(fsreqchan, r);
+}
+
+static void
+fsdestroyfid(Fid *f)
+{
+	free(f->aux);
+}
+
+void
+fsinit0(void)	/* bad planning - clash with lib9pclient */
+{
+	t0 = time(0);
+
+	fs.attach = fssend;
+	fs.flush = fssend;
+	fs.open = fssend;
+	fs.walk = fssend;
+	fs.read = fssend;
+	fs.write = fssend;
+	fs.stat = fssend;
+	fs.destroyfid = fsdestroyfid;
+	
+	rootqid = qid(Qroot, nil, nil, nil);
+	
+	fsreqchan = chancreate(sizeof(void*), 0);
+	mailthread(fsrecv, nil);
+}
+
diff --git a/src/cmd/upas/nfs/fs.h b/src/cmd/upas/nfs/fs.h
new file mode 100644
index 0000000..83ff8ba
--- /dev/null
+++ b/src/cmd/upas/nfs/fs.h
@@ -0,0 +1,2 @@
+extern Srv fs;
+void fsinit0(void);
diff --git a/src/cmd/upas/nfs/imap.c b/src/cmd/upas/nfs/imap.c
new file mode 100644
index 0000000..80ca59c
--- /dev/null
+++ b/src/cmd/upas/nfs/imap.c
@@ -0,0 +1,1715 @@
+/*
+ * Locking here is not quite right.
+ * Calling qlock(&z->lk) can block the proc,
+ * and when it comes back, boxes and msgs might have been freed
+ * (if the refresh proc was holding the lock and in the middle of a
+ * redial).  I've tried to be careful about not assuming boxes continue
+ * to exist across imap commands, but maybe this isn't really tenable.
+ * Maybe instead we should ref count the boxes and messages.
+ */
+
+#include "a.h"
+#include <libsec.h>
+
+struct Imap
+{
+	int		connected;
+	int		autoreconnect;
+	int		ticks;	/* until boom! */
+	char*	server;
+	int		mode;
+	int		fd;
+	Biobuf	b;
+	Ioproc*	io;
+	QLock	lk;
+	QLock	rlk;
+	Rendez	r;
+
+	Box*		inbox;
+	Box*		box;
+	Box*		nextbox;
+
+	/* SEARCH results */
+	uint		*uid;
+	uint		nuid;
+};
+
+static struct {
+	char *name;
+	int flag;
+} flagstab[] = 
+{
+	"Junk",	FlagJunk,
+	"NonJunk",	FlagNonJunk,
+	"\\Answered",	FlagReplied,
+	"\\Flagged",	FlagFlagged,
+	"\\Deleted",	FlagDeleted,
+	"\\Draft",		FlagDraft,
+	"\\Seen",		FlagSeen,
+	"\\NoInferiors",	FlagNoInferiors,
+	"\\NoSelect",	FlagNoSelect,
+	"\\Marked",	FlagMarked,
+	"\\UnMarked",	FlagUnMarked,
+};
+
+int			chattyimap;
+
+static char	*tag = "+";
+
+static void	checkbox(Imap*, Box*);
+static char*	copyaddrs(Sx*);
+static void	freeup(UserPasswd*);
+static int		getbox(Imap*, Box*);
+static int		getboxes(Imap*);
+static char*	gsub(char*, char*, char*);
+static int		imapcmd(Imap*, Box*, char*, ...);
+static Sx*		imapcmdsx(Imap*, Box*, char*, ...);
+static Sx*		imapcmdsx0(Imap*, char*, ...);
+static Sx*		imapvcmdsx(Imap*, Box*, char*, va_list);
+static Sx*		imapvcmdsx0(Imap*, char*, va_list);
+static int		imapdial(char*, int);
+static int		imaplogin(Imap*);
+static int		imapquote(Fmt*);
+static int		imapreconnect(Imap*);
+static void	imaprefreshthread(void*);
+static void	imaptimerproc(void*);
+static Sx*		imapwaitsx(Imap*);
+static int		isatom(Sx *v, char *name);
+static int		islist(Sx *v);
+static int		isnil(Sx *v);
+static int		isnumber(Sx *sx);
+static int		isstring(Sx *sx);
+static int		ioimapdial(Ioproc*, char*, int);
+static char*	nstring(Sx*);
+static void	unexpected(Imap*, Sx*);
+static Sx*		zBrdsx(Imap*);
+
+/*
+ * Imap connection maintenance and login.
+ */
+
+Imap*
+imapconnect(char *server, int mode)
+{
+	Imap *z;
+
+	fmtinstall('H', encodefmt);
+	fmtinstall('Z', imapquote);
+
+	z = emalloc(sizeof *z);
+	z->server = estrdup(server);
+	z->mode = mode;
+	z->fd = -1;
+	z->autoreconnect = 0;
+	z->io = ioproc();
+	
+	qlock(&z->lk);
+	if(imapreconnect(z) < 0){
+		free(z);
+		return nil;
+	}
+	
+	z->r.l = &z->rlk;
+	z->autoreconnect = 1;
+	qunlock(&z->lk);
+	
+	proccreate(imaptimerproc, z, STACK);
+	mailthread(imaprefreshthread, z);
+	
+	return z;
+}
+
+void
+imaphangup(Imap *z, int ticks)
+{
+	z->ticks = ticks;
+	if(ticks == 0){
+		close(z->fd);
+		z->fd = -1;
+	}
+}
+
+static int
+imapreconnect(Imap *z)
+{
+	Sx *sx;
+
+	z->autoreconnect = 0;
+	z->box = nil;
+	z->inbox = nil;
+
+	if(z->fd >= 0){
+		close(z->fd);
+		z->fd = -1;
+	}
+
+	if(chattyimap)
+		fprint(2, "dial %s...\n", z->server);
+	if((z->fd = ioimapdial(z->io, z->server, z->mode)) < 0)
+		return -1;
+	z->connected = 1;
+	Binit(&z->b, z->fd, OREAD);
+	if((sx = zBrdsx(z)) == nil){
+		werrstr("no greeting");
+		goto err;
+	}
+	if(chattyimap)
+		fprint(2, "<I %#$\n", sx);
+	if(sx->nsx >= 2 && isatom(sx->sx[0], "*") && isatom(sx->sx[1], "PREAUTH")){
+		freesx(sx);
+		goto preauth;
+	}
+	if(!oksx(sx)){
+		werrstr("bad greeting - %#$", sx);
+		goto err;
+	}
+	freesx(sx);
+	sx = nil;
+	if(imaplogin(z) < 0)
+		goto err;
+preauth:
+	if(getboxes(z) < 0 || getbox(z, z->inbox) < 0)
+		goto err;
+	z->autoreconnect = 1;
+	return 0;
+
+err:
+	if(z->fd >= 0){
+		close(z->fd);
+		z->fd = -1;
+	}
+	if(sx)
+		freesx(sx);
+	z->autoreconnect = 1;
+	z->connected = 0;
+	return -1;
+}
+
+static int
+imaplogin(Imap *z)
+{
+	Sx *sx;
+	UserPasswd *up;
+
+	if((up = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=imap server=%q", z->server)) == nil){
+		werrstr("getuserpasswd - %r");
+		return -1;
+	}
+
+	sx = imapcmdsx(z, nil, "LOGIN %Z %Z", up->user, up->passwd);
+	freeup(up);
+	if(sx == nil)
+		return -1;
+	if(!oksx(sx)){
+		freesx(sx);
+		werrstr("login rejected - %#$", sx);
+		return -1;
+	}
+	return 0;
+}
+
+static int
+getboxes(Imap *z)
+{
+	int i;
+	Box **r, **w, **e;
+	
+	for(i=0; i<nboxes; i++){
+		boxes[i]->mark = 1;
+		boxes[i]->exists = 0;
+		boxes[i]->maxseen = 0;
+	}
+	if(imapcmd(z, nil, "LIST %Z *", "") < 0)
+		return -1;
+	if(z->nextbox && z->nextbox->mark)
+		z->nextbox = nil;
+	for(r=boxes, w=boxes, e=boxes+nboxes; r<e; r++){
+		if((*r)->mark)
+{fprint(2, "*** free box %s %s\n", (*r)->name, (*r)->imapname);
+			boxfree(*r);
+}
+		else
+			*w++ = *r;
+	}
+	nboxes = w - boxes;
+	return 0;
+}
+
+static int
+getbox(Imap *z, Box *b)
+{
+	int i;
+	Msg **r, **w, **e;
+	
+	if(b == nil)
+		return 0;
+	
+	for(i=0; i<b->nmsg; i++)
+		b->msg[i]->imapid = 0;
+	if(imapcmd(z, b, "UID FETCH 1:* FLAGS") < 0)
+		return -1;
+	for(r=b->msg, w=b->msg, e=b->msg+b->nmsg; r<e; r++){
+		if((*r)->imapid == 0)
+			msgfree(*r);
+		else{
+			(*r)->ix = w-b->msg;
+			*w++ = *r;
+		}
+	}
+	b->nmsg = w - b->msg;
+	b->imapinit = 1;
+	checkbox(z, b);
+	return 0;
+}
+
+static void
+freeup(UserPasswd *up)
+{
+	memset(up->user, 0, strlen(up->user));
+	memset(up->passwd, 0, strlen(up->passwd));
+	free(up);
+}
+
+static void
+imaptimerproc(void *v)
+{
+	Imap *z;
+	
+	z = v;
+	for(;;){
+		sleep(60*1000);
+		qlock(z->r.l);
+		rwakeup(&z->r);
+		qunlock(z->r.l);
+	}
+}
+
+static void
+checkbox(Imap *z, Box *b)
+{
+	if(imapcmd(z, b, "NOOP") >= 0){
+		if(!b->imapinit)
+			getbox(z, b);
+		if(!b->imapinit)
+			return;
+		if(b==z->box && b->exists > b->maxseen){
+			imapcmd(z, b, "UID FETCH %d:* FULL",
+				b->uidnext);
+		}
+	}
+}
+
+static void
+imaprefreshthread(void *v)
+{
+	Imap *z;
+	
+	z = v;
+	for(;;){
+		qlock(z->r.l);
+		rsleep(&z->r);
+		qunlock(z->r.l);
+		
+		qlock(&z->lk);
+		if(z->inbox)
+			checkbox(z, z->inbox);
+		qunlock(&z->lk);
+	}
+}
+
+/*
+ * Run a single command and return the Sx.  Does NOT redial.
+ */
+static Sx*
+imapvcmdsx0(Imap *z, char *fmt, va_list arg)
+{
+	char *s;
+	Fmt f;
+	int prefix, len;
+	Sx *sx;
+	
+	if(canqlock(&z->lk))
+		abort();
+
+	if(z->fd < 0 || !z->connected)
+		return nil;
+
+	prefix = strlen(tag)+1;
+	fmtstrinit(&f);
+	fmtprint(&f, "%s ", tag);
+	fmtvprint(&f, fmt, arg);
+	fmtprint(&f, "\r\n");
+	s = fmtstrflush(&f);
+	len = strlen(s);
+	s[len-2] = 0;
+	if(chattyimap)
+		fprint(2, "I> %s\n", s);
+	s[len-2] = '\r';
+	if(iowrite(z->io, z->fd, s, len) < 0){
+		z->connected = 0;
+		free(s);
+		return nil;
+	}
+	sx = imapwaitsx(z);
+	free(s);
+	return sx;
+}
+
+static Sx*
+imapcmdsx0(Imap *z, char *fmt, ...)
+{
+	va_list arg;
+	Sx *sx;
+	
+	va_start(arg, fmt);
+	sx = imapvcmdsx0(z, fmt, arg);
+	va_end(arg);
+	return sx;
+}
+
+/*
+ * Run a single command on box b.  Does redial.
+ */
+static Sx*
+imapvcmdsx(Imap *z, Box *b, char *fmt, va_list arg)
+{
+	int tries;
+	Sx *sx;
+	
+	tries = 0;
+	z->nextbox = b;
+
+	if(z->fd < 0 || !z->connected){
+reconnect:
+		if(!z->autoreconnect)
+			return nil;
+		if(imapreconnect(z) < 0)
+			return nil;
+		if(b && z->nextbox == nil)	/* box disappeared on reconnect */
+			return nil;
+	}
+
+	if(b && b != z->box){
+		if(z->box)
+			z->box->imapinit = 0;
+		z->box = b;
+		if((sx=imapcmdsx0(z, "SELECT %Z", b->imapname)) == nil){
+			z->box = nil;
+			if(tries++ == 0 && (z->fd < 0 || !z->connected))
+				goto reconnect;
+			return nil;
+		}
+		freesx(sx);
+	}
+
+	if((sx=imapvcmdsx0(z, fmt, arg)) == nil){
+		if(tries++ == 0 && (z->fd < 0 || !z->connected))
+			goto reconnect;
+		return nil;
+	}
+	return sx;
+}
+
+static int
+imapcmd(Imap *z, Box *b, char *fmt, ...)
+{
+	Sx *sx;
+	va_list arg;
+
+	va_start(arg, fmt);
+	sx = imapvcmdsx(z, b, fmt, arg);
+	va_end(arg);
+	if(sx == nil)
+		return -1;
+	if(sx->nsx < 2 || !isatom(sx->sx[1], "OK")){
+		werrstr("%$", sx);
+		freesx(sx);
+		return -1;
+	}
+	freesx(sx);
+	return 0;
+}
+
+static Sx*
+imapcmdsx(Imap *z, Box *b, char *fmt, ...)
+{
+	Sx *sx;
+	va_list arg;
+
+	va_start(arg, fmt);
+	sx = imapvcmdsx(z, b, fmt, arg);
+	va_end(arg);
+	return sx;
+}
+
+static Sx*
+imapwaitsx(Imap *z)
+{
+	Sx *sx;
+	
+	while((sx = zBrdsx(z)) != nil){
+		if(chattyimap)
+			fprint(2, "<| %#$\n", sx);
+		if(sx->nsx >= 1 && sx->sx[0]->type == SxAtom && cistrcmp(sx->sx[0]->data, tag) == 0)
+			return sx;
+		if(sx->nsx >= 1 && sx->sx[0]->type == SxAtom && strcmp(sx->sx[0]->data, "*") == 0)
+			unexpected(z, sx);
+		if(sx->type == SxList && sx->nsx == 0){
+			freesx(sx);
+			break;
+		}
+		freesx(sx);
+	}
+	z->connected = 0;
+	return nil;
+}
+
+/*
+ * Imap interface to mail file system.
+ */
+
+static void
+_bodyname(char *buf, char *ebuf, Part *p, char *extra)
+{
+	if(buf >= ebuf){
+		fprint(2, "***** BUFFER TOO SMALL\n");
+		return;
+	}
+	*buf = 0;
+	if(p->parent){
+		_bodyname(buf, ebuf, p->parent, "");
+		buf += strlen(buf);
+		seprint(buf, ebuf, ".%d", p->pix+1);
+	}
+	buf += strlen(buf);
+	seprint(buf, ebuf, "%s", extra);
+}
+
+static char*
+bodyname(Part *p, char *extra)
+{
+	static char buf[256];
+	memset(buf, 0, sizeof buf);	/* can't see why this is necessary, but it is */
+	_bodyname(buf, buf+sizeof buf, p, extra);
+	return buf+1;	/* buf[0] == '.' */
+}
+
+static void
+fetch1(Imap *z, Part *p, char *s)
+{
+	qlock(&z->lk);
+	imapcmd(z, p->msg->box, "UID FETCH %d BODY[%s]", 
+		p->msg->imapuid, bodyname(p, s));
+	qunlock(&z->lk);
+}
+
+void
+imapfetchrawheader(Imap *z, Part *p)
+{
+	fetch1(z, p, ".HEADER");
+}
+
+void
+imapfetchrawmime(Imap *z, Part *p)
+{
+	fetch1(z, p, ".MIME");
+}
+
+void
+imapfetchrawbody(Imap *z, Part *p)
+{
+	fetch1(z, p, ".TEXT");
+}
+
+void
+imapfetchraw(Imap *z, Part *p)
+{
+	fetch1(z, p, "");
+}
+
+static int
+imaplistcmd(Imap *z, Box *box, char *before, Msg **m, uint nm, char *after)
+{
+	int i, r;
+	char *cmd;
+	Fmt fmt;
+	
+	if(nm == 0)
+		return 0;
+
+	fmtstrinit(&fmt);
+	fmtprint(&fmt, "%s ", before);
+	for(i=0; i<nm; i++){
+		if(i > 0)
+			fmtrune(&fmt, ',');
+		fmtprint(&fmt, "%ud", m[i]->imapuid);
+	}
+	fmtprint(&fmt, " %s", after);
+	cmd = fmtstrflush(&fmt);
+	
+	r = 0;
+	if(imapcmd(z, box, "%s", cmd) < 0)
+		 r = -1;
+	free(cmd);
+	return r;
+}
+
+int
+imapcopylist(Imap *z, char *nbox, Msg **m, uint nm)
+{
+	int rv;
+	char *name;
+	
+	if(nm == 0)
+		return 0;
+
+	qlock(&z->lk);
+	name = esmprint("%Z", nbox);
+	rv = imaplistcmd(z, m[0]->box, "UID COPY", m, nm, name);
+	free(name);
+	qunlock(&z->lk);
+	return rv;
+}
+
+int
+imapremovelist(Imap *z, Msg **m, uint nm)
+{
+	int rv;
+	
+	if(nm == 0)
+		return 0;
+
+	qlock(&z->lk);
+	rv = imaplistcmd(z, m[0]->box, "UID STORE", m, nm, "+FLAGS.SILENT (\\Deleted)");
+	/* careful - box might be gone; use z->box instead */
+	if(rv == 0 && z->box)
+		rv = imapcmd(z, z->box, "EXPUNGE");
+	qunlock(&z->lk);
+	return rv;
+}
+
+int
+imapflaglist(Imap *z, int op, int flag, Msg **m, uint nm)
+{
+	char *mod, *s, *sep;
+	int i, rv;
+	Fmt fmt;
+	
+	if(op > 0)
+		mod = "+";
+	else if(op == 0)
+		mod = "";
+	else
+		mod = "-";
+
+	fmtstrinit(&fmt);
+	fmtprint(&fmt, "%sFLAGS (", mod);
+	sep = "";
+	for(i=0; i<nelem(flagstab); i++){
+		if(flagstab[i].flag & flag){
+			fmtprint(&fmt, "%s%s", sep, flagstab[i].name);
+			sep = " ";
+		}
+	}
+	fmtprint(&fmt, ")");
+	s = fmtstrflush(&fmt);
+	
+	qlock(&z->lk);
+	rv = imaplistcmd(z, m[0]->box, "UID STORE", m, nm, s);
+	qunlock(&z->lk);
+	free(s);
+	return rv;
+}
+
+int
+imapsearchbox(Imap *z, Box *b, char *search, Msg ***mm)
+{
+	uint *uid;
+	int i, nuid;
+	Msg **m;
+	int nm;
+
+	qlock(&z->lk);
+	if(imapcmd(z, b, "UID SEARCH CHARSET UTF-8 TEXT %Z", search) < 0){
+		qunlock(&z->lk);
+		return -1;
+	}
+
+	uid = z->uid;
+	nuid = z->nuid;
+	z->uid = nil;
+	z->nuid = 0;
+	qunlock(&z->lk);
+
+	m = emalloc(nuid*sizeof m[0]);
+	nm = 0;
+	for(i=0; i<nuid; i++)
+		if((m[nm] = msgbyimapuid(b, uid[i], 0)) != nil)
+			nm++;
+	*mm = m;
+	free(uid);
+	return nm;
+}
+
+void
+imapcheckbox(Imap *z, Box *b)
+{
+	if(b == nil)
+		return;
+	qlock(&z->lk);
+	checkbox(z, b);
+	qunlock(&z->lk);
+}
+
+/*
+ * Imap utility routines
+ */
+static long
+_ioimapdial(va_list *arg)
+{
+	char *server;
+	int mode;
+	
+	server = va_arg(*arg, char*);
+	mode = va_arg(*arg, int);
+	return imapdial(server, mode);
+}
+static int
+ioimapdial(Ioproc *io, char *server, int mode)
+{
+	return iocall(io, _ioimapdial, server, mode);
+}
+
+static long
+_ioBrdsx(va_list *arg)
+{
+	Biobuf *b;
+	Sx **sx;
+	
+	b = va_arg(*arg, Biobuf*);
+	sx = va_arg(*arg, Sx**);
+	*sx = Brdsx(b);
+	if((*sx) && (*sx)->type == SxList && (*sx)->nsx == 0){
+		freesx(*sx);
+		*sx = nil;
+	}
+	return 0;
+}
+static Sx*
+ioBrdsx(Ioproc *io, Biobuf *b)
+{
+	Sx *sx;
+
+	iocall(io, _ioBrdsx, b, &sx);
+	return sx;
+}
+
+static Sx*
+zBrdsx(Imap *z)
+{
+	if(z->ticks && --z->ticks==0){
+		close(z->fd);
+		z->fd = -1;
+		return nil;
+	}
+	return ioBrdsx(z->io, &z->b);
+}
+
+static int
+imapdial(char *server, int mode)
+{
+	int p[2];
+	int fd[3];
+	char *tmp;
+	
+	switch(mode){
+	default:
+	case Unencrypted:
+		return dial(netmkaddr(server, "tcp", "143"), nil, nil, nil);
+		
+	case Starttls:
+		werrstr("starttls not supported");
+		return -1;
+
+	case Tls:
+		if(pipe(p) < 0)
+			return -1;
+		fd[0] = dup(p[0], -1);
+		fd[1] = dup(p[0], -1);
+		fd[2] = dup(2, -1);
+		tmp = esmprint("%s:993", server);
+		if(threadspawnl(fd, "/usr/sbin/stunnel", "stunnel", "-c", "-r", tmp, nil) < 0){
+			free(tmp);
+			close(p[0]);
+			close(p[1]);
+			close(fd[0]);
+			close(fd[1]);
+			close(fd[2]);
+			return -1;
+		}
+		free(tmp);
+		close(p[0]);
+		return p[1];
+	
+	case Cmd:
+		if(pipe(p) < 0)
+			return -1;
+		fd[0] = dup(p[0], -1);
+		fd[1] = dup(p[0], -1);
+		fd[2] = dup(2, -1);
+		if(threadspawnl(fd, "/usr/local/plan9/bin/rc", "rc", "-c", server, nil) < 0){
+			close(p[0]);
+			close(p[1]);
+			close(fd[0]);
+			close(fd[1]);
+			close(fd[2]);
+			return -1;
+		}
+		close(p[0]);
+		return p[1];
+	}
+}
+
+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;
+}
+
+static int
+imapquote(Fmt *f)
+{
+	char *s, *t;
+	int w, quotes;
+	Rune r;
+
+	s = va_arg(f->args, char*);
+	if(s == nil || *s == '\0')
+		return fmtstrcpy(f, "\"\"");
+
+	quotes = 0;
+	if(f->flags&FmtSharp)
+		quotes = 1;
+	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
+fmttype(char c)
+{
+	switch(c){
+	case 'A':
+		return SxAtom;
+	case 'L':
+		return SxList;
+	case 'N':
+		return SxNumber;
+	case 'S':
+		return SxString;
+	default:
+		return -1;
+	}
+}
+
+/*
+ * Check S expression against format string.
+ */
+static int
+sxmatch(Sx *sx, char *fmt)
+{
+	int i;
+	
+	for(i=0; fmt[i]; i++){
+		if(fmt[i] == '*')
+			fmt--;	/* like i-- but better */
+		if(i == sx->nsx && fmt[i+1] == '*')
+			return 1;
+		if(i >= sx->nsx)
+			return 0;
+		if(sx->sx[i] == nil)
+			return 0;
+		if(sx->sx[i]->type == SxAtom && strcmp(sx->sx[i]->data, "NIL") == 0){
+			if(fmt[i] == 'L'){
+				free(sx->sx[i]->data);
+				sx->sx[i]->data = nil;
+				sx->sx[i]->type = SxList;
+				sx->sx[i]->sx = nil;
+				sx->sx[i]->nsx = 0;
+			}
+			else if(fmt[i] == 'S'){
+				free(sx->sx[i]->data);
+				sx->sx[i]->data = nil;
+				sx->sx[i]->type = SxString;
+			}
+		}
+		if(sx->sx[i]->type == SxAtom && fmt[i]=='S')
+			sx->sx[i]->type = SxString;
+		if(sx->sx[i]->type != fmttype(fmt[i])){
+			fprint(2, "sxmatch: %$ not %c\n", sx->sx[i], fmt[i]);
+			return 0;
+		}
+	}
+	if(i != sx->nsx)
+		return 0;
+	return 1;
+}
+
+/*
+ * Check string against format string.
+ */
+static int
+stringmatch(char *fmt, char *s)
+{
+	for(; *fmt && *s; fmt++, s++){
+		switch(*fmt){
+		case '0':
+			if(*s == ' ')
+				break;
+			/* fall through */
+		case '1':
+			if(*s < '0' || *s > '9')
+				return 0;
+			break;
+		case 'A':
+			if(*s < 'A' || *s > 'Z')
+				return 0;
+			break;
+		case 'a':
+			if(*s < 'a' || *s > 'z')
+				return 0;
+			break;
+		case '+':
+			if(*s != '-' && *s != '+')
+				return 0;
+			break;
+		default:
+			if(*s != *fmt)
+				return 0;
+			break;
+		}
+	}
+	if(*fmt || *s)
+		return 0;
+	return 1;
+}
+
+/*
+ * Parse simple S expressions and IMAP elements.
+ */
+static int
+isatom(Sx *v, char *name)
+{
+	int n;
+	
+	if(v == nil || v->type != SxAtom)
+		return 0;
+	n = strlen(name);
+	if(cistrncmp(v->data, name, n) == 0)
+		if(v->data[n] == 0 || (n>0 && v->data[n-1] == '['))
+			return 1;
+	return 0;
+}
+
+static int
+isstring(Sx *sx)
+{
+	if(sx->type == SxAtom)
+		sx->type = SxString;
+	return sx->type == SxString;
+}
+
+static int
+isnumber(Sx *sx)
+{
+	return sx->type == SxNumber;
+}
+
+static int
+isnil(Sx *v)
+{
+	return v == nil || 
+		(v->type==SxList && v->nsx == 0) ||
+		(v->type==SxAtom && strcmp(v->data, "NIL") == 0);
+}
+
+static int
+islist(Sx *v)
+{
+	return isnil(v) || v->type==SxList;
+}
+
+static uint
+parseflags(Sx *v)
+{
+	int f, i, j;
+	
+	if(v->type != SxList){
+		warn("malformed flags: %$", v);
+		return 0;
+	}
+	f = 0;
+	for(i=0; i<v->nsx; i++){
+		if(v->sx[i]->type != SxAtom)
+			continue;
+		for(j=0; j<nelem(flagstab); j++)
+			if(cistrcmp(v->sx[i]->data, flagstab[j].name) == 0)
+				f |= flagstab[j].flag;
+	}
+	return f;
+}
+	
+static char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
+static int
+parsemon(char *s)
+{
+	int i;
+	
+	for(i=0; months[i]; i+=3)
+		if(memcmp(s, months+i, 3) == 0)
+			return i/3;
+	return -1;
+}
+
+static uint
+parsedate(Sx *v)
+{
+	Tm tm;
+	uint t;
+	int delta;
+	char *p;
+	
+	if(v->type != SxString || !stringmatch("01-Aaa-1111 01:11:11 +1111", v->data)){
+	bad:
+		warn("bad date: %$", v);
+		return 0;
+	}
+	memset(&tm, 0, sizeof tm);
+	p = v->data;
+	tm.mday = atoi(p);
+	tm.mon = parsemon(p+3);
+	if(tm.mon == -1)
+		goto bad;
+	tm.year = atoi(p+7) - 1900;
+	tm.hour = atoi(p+12);
+	tm.min = atoi(p+15);
+	tm.sec = atoi(p+18);
+	strcpy(tm.zone, "GMT");
+
+	t = tm2sec(&tm);
+	delta = ((p[22]-'0')*10+p[23]-'0')*3600 + ((p[24]-'0')*10+p[25]-'0')*60;
+	if(p[21] == '-')
+		delta = -delta;
+	
+	t -= delta;
+	return t;
+}
+
+static uint
+parsenumber(Sx *v)
+{
+	if(v->type != SxNumber)
+		return 0;
+	return v->number;
+}
+
+static void
+hash(DigestState *ds, char *tag, char *val)
+{
+	if(val == nil)
+		val = "";
+	md5((uchar*)tag, strlen(tag)+1, nil, ds);
+	md5((uchar*)val, strlen(val)+1, nil, ds);
+}
+
+static Hdr*
+parseenvelope(Sx *v)
+{
+	Hdr *hdr;
+	uchar digest[16];
+	DigestState ds;
+	
+	if(v->type != SxList || !sxmatch(v, "SSLLLLLLSS")){
+		warn("bad envelope: %$", v);
+		return nil;
+	}
+
+	hdr = emalloc(sizeof *hdr);
+	hdr->date = nstring(v->sx[0]);
+	hdr->subject = unrfc2047(nstring(v->sx[1]));
+	hdr->from = copyaddrs(v->sx[2]);
+	hdr->sender = copyaddrs(v->sx[3]);
+	hdr->replyto = copyaddrs(v->sx[4]);
+	hdr->to = copyaddrs(v->sx[5]);
+	hdr->cc = copyaddrs(v->sx[6]);
+	hdr->bcc = copyaddrs(v->sx[7]);
+	hdr->inreplyto = unrfc2047(nstring(v->sx[8]));
+	hdr->messageid = unrfc2047(nstring(v->sx[9]));
+	
+	memset(&ds, 0, sizeof ds);
+	hash(&ds, "date", hdr->date);
+	hash(&ds, "subject", hdr->subject);
+	hash(&ds, "from", hdr->from);
+	hash(&ds, "sender", hdr->sender);
+	hash(&ds, "replyto", hdr->replyto);
+	hash(&ds, "to", hdr->to);
+	hash(&ds, "cc", hdr->cc);
+	hash(&ds, "bcc", hdr->bcc);
+	hash(&ds, "inreplyto", hdr->inreplyto);
+	hash(&ds, "messageid", hdr->messageid);
+	md5(0, 0, digest, &ds);
+	hdr->digest = esmprint("%.16H", digest);
+
+	return hdr;
+}
+
+static void
+strlwr(char *s)
+{
+	char *t;
+	
+	if(s == nil)
+		return;
+	for(t=s; *t; t++)
+		if('A' <= *t && *t <= 'Z')
+			*t += 'a' - 'A';
+}
+
+static void
+nocr(char *s)
+{
+	char *r, *w;
+	
+	if(s == nil)
+		return;
+	for(r=w=s; *r; r++)
+		if(*r != '\r')
+			*w++ = *r;
+	*w = 0;
+}
+
+/*
+ * substitute all occurrences of a with b in s.
+ */
+static char*
+gsub(char *s, char *a, char *b)
+{
+	char *p, *t, *w, *last;
+	int n;
+	
+	n = 0;
+	for(p=s; (p=strstr(p, a)) != nil; p+=strlen(a))
+		n++;
+	if(n == 0)
+		return s;
+	t = emalloc(strlen(s)+n*strlen(b)+1);
+	w = t;
+	for(p=s; last=p, (p=strstr(p, a)) != nil; p+=strlen(a)){
+		memmove(w, last, p-last);
+		w += p-last;
+		memmove(w, b, strlen(b));
+		w += strlen(b);
+	}
+	strcpy(w, last);
+	free(s);
+	return t;
+}
+
+/*
+ * Table-driven IMAP "unexpected response" parser.
+ * All the interesting data is in the unexpected responses.
+ */
+static void xlist(Imap*, Sx*);
+static void xrecent(Imap*, Sx*);
+static void xexists(Imap*, Sx*);
+static void xok(Imap*, Sx*);
+static void xflags(Imap*, Sx*);
+static void xfetch(Imap*, Sx*);
+static void xexpunge(Imap*, Sx*);
+static void xbye(Imap*, Sx*);
+static void xsearch(Imap*, Sx*);
+
+static struct {
+	int		num;
+	char		*name;
+	char		*fmt;
+	void		(*fn)(Imap*, Sx*);
+} unextab[] = {
+	0,	"BYE",		nil,			xbye,
+	0,	"FLAGS",		"AAL",		xflags,
+	0,	"LIST",		"AALSS",		xlist,
+	0,	"OK",		nil,			xok,
+	0,	"SEARCH",	"AAN*",		xsearch,
+
+	1,	"EXISTS",		"ANA",		xexists,
+	1,	"EXPUNGE",	"ANA",		xexpunge,
+	1,	"FETCH",		"ANAL",		xfetch,
+	1,	"RECENT",	"ANA",		xrecent,
+};
+
+static void
+unexpected(Imap *z, Sx *sx)
+{
+	int i, num;
+	char *name;
+
+	if(sx->nsx >= 3 && sx->sx[1]->type == SxNumber && sx->sx[2]->type == SxAtom){
+		num = 1;
+		name = sx->sx[2]->data;
+	}else if(sx->nsx >= 2 && sx->sx[1]->type == SxAtom){
+		num = 0;
+		name = sx->sx[1]->data;
+	}else
+		return;
+	
+	for(i=0; i<nelem(unextab); i++){
+		if(unextab[i].num == num && cistrcmp(unextab[i].name, name) == 0){
+			if(unextab[i].fmt && !sxmatch(sx, unextab[i].fmt)){
+				warn("malformed %s: %$", name, sx);
+				continue;
+			}
+			unextab[i].fn(z, sx);
+		}
+	}
+}
+
+
+static void
+xlist(Imap *z, Sx *sx)
+{
+	int inbox;
+	char *s, *t;
+	Box *box;
+
+	if(sx->sx[3]->data && strcmp(sx->sx[3]->data, "/") != 0)
+		warn("box separator %q not / - need to implement translation", sx->sx[3]->data);
+	s = estrdup(sx->sx[4]->data);
+	if(sx->sx[3]->data && strcmp(sx->sx[3]->data, "/") != 0){
+		s = gsub(s, "/", "_");
+		s = gsub(s, sx->sx[3]->data, "/");
+	}
+	
+	/* 
+	 * Plan 9 calls the main mailbox mbox.  
+	 * Rename any existing mbox by appending a $.
+	 */
+	inbox = 0;
+	if(strncmp(s, "mbox", 4) == 0){
+		t = emalloc(strlen(s)+2);
+		strcpy(t, s);
+		strcat(t, "$");
+		free(s);
+		s = t;
+	}else if(cistrcmp(s, "INBOX") == 0){
+		inbox = 1;
+		free(s);
+		s = estrdup("mbox");
+	}
+
+	box = boxcreate(s);
+	if(box == nil)
+		return;
+	box->imapname = estrdup(sx->sx[4]->data);
+	if(inbox)
+		z->inbox = box;
+	box->mark = 0;
+	box->flags = parseflags(sx->sx[2]);
+}
+
+static void
+xrecent(Imap *z, Sx *sx)
+{
+	if(z->box)
+		z->box->recent = sx->sx[1]->number;
+}
+
+static void
+xexists(Imap *z, Sx *sx)
+{
+	if(z->box){
+		z->box->exists = sx->sx[1]->number;
+		if(z->box->exists < z->box->maxseen)
+			z->box->maxseen = z->box->exists;
+	}
+}
+
+static void
+xflags(Imap *z, Sx *sx)
+{
+	if(z->box)
+		z->box->flags = parseflags(sx->sx[2]);
+}
+
+static void
+xbye(Imap *z, Sx *sx)
+{
+	close(z->fd);
+	z->fd = -1;
+	z->connected = 0;
+}
+
+static void
+xexpunge(Imap *z, Sx *sx)
+{
+	int i, n;
+	Box *b;
+
+	if((b=z->box) == nil)
+		return;
+	n = sx->sx[1]->number;
+	for(i=0; i<b->nmsg; i++){
+		if(b->msg[i]->imapid == n){
+			msgplumb(b->msg[i], 1);
+			msgfree(b->msg[i]);
+			b->nmsg--;
+			memmove(b->msg+i, b->msg+i+1, (b->nmsg-i)*sizeof b->msg[0]);
+			i--;
+			b->maxseen--;
+			b->exists--;
+			continue;
+		}
+		if(b->msg[i]->imapid > n)
+			b->msg[i]->imapid--;
+		b->msg[i]->ix = i;
+	}
+}
+
+static void
+xsearch(Imap *z, Sx *sx)
+{
+	int i;
+	
+	free(z->uid);
+	z->uid = emalloc((sx->nsx-2)*sizeof z->uid[0]);
+	z->nuid = sx->nsx-2;
+	for(i=0; i<z->nuid; i++)
+		z->uid[i] = sx->sx[i+2]->number;
+}
+
+/* 
+ * Table-driven FETCH message info parser.
+ */
+static void xmsgflags(Msg*, Sx*, Sx*);
+static void xmsgdate(Msg*, Sx*, Sx*);
+static void xmsgrfc822size(Msg*, Sx*, Sx*);
+static void xmsgenvelope(Msg*, Sx*, Sx*);
+static void xmsgbody(Msg*, Sx*, Sx*);
+static void xmsgbodydata(Msg*, Sx*, Sx*);
+
+static struct {
+	char *name;
+	void (*fn)(Msg*, Sx*, Sx*);
+} msgtab[] = {
+	"FLAGS", xmsgflags,
+	"INTERNALDATE", xmsgdate,
+	"RFC822.SIZE", xmsgrfc822size,
+	"ENVELOPE", xmsgenvelope,
+	"BODY", xmsgbody,
+	"BODY[", xmsgbodydata,
+};
+
+static void
+xfetch(Imap *z, Sx *sx)
+{
+	int i, j, n, uid;
+	Msg *msg;
+
+	if(z->box == nil){
+		warn("FETCH but no open box: %$", sx);
+		return;
+	}
+
+	/* * 152 FETCH (UID 185 FLAGS () ...) */
+	if(sx->sx[3]->nsx%2){
+		warn("malformed FETCH: %$", sx);
+		return;
+	}
+
+	n = sx->sx[1]->number;
+	sx = sx->sx[3];
+	for(i=0; i<sx->nsx; i+=2){
+		if(isatom(sx->sx[i], "UID")){
+			if(sx->sx[i+1]->type == SxNumber){
+				uid = sx->sx[i+1]->number;
+				goto haveuid;
+			}
+		}
+	}
+	warn("FETCH without UID: %$", sx);
+	return;
+
+haveuid:
+	msg = msgbyimapuid(z->box, uid, 1);
+	if(msg->imapid && msg->imapid != n)
+		warn("msg id mismatch: want %d have %d", msg->id, n);
+	msg->imapid = n;
+	for(i=0; i<sx->nsx; i+=2){
+		for(j=0; j<nelem(msgtab); j++)
+			if(isatom(sx->sx[i], msgtab[j].name))
+				msgtab[j].fn(msg, sx->sx[i], sx->sx[i+1]);
+	}
+}
+
+static void
+xmsgflags(Msg *msg, Sx *k, Sx *v)
+{
+	USED(k);
+	msg->flags = parseflags(v);
+}
+
+static void
+xmsgdate(Msg *msg, Sx *k, Sx *v)
+{
+	USED(k);
+	msg->date = parsedate(v);
+}
+
+static void
+xmsgrfc822size(Msg *msg, Sx *k, Sx *v)
+{
+	USED(k);
+	msg->size = parsenumber(v);
+}
+
+static char*
+nstring(Sx *v)
+{
+	char *p;
+	
+	p = v->data;
+	v->data = nil;
+	return p;
+}
+
+static char*
+copyaddrs(Sx *v)
+{
+	char *s, *sep;
+	char *name, *email, *host, *mbox;
+	int i;
+	Fmt fmt;
+	
+	if(v->nsx == 0)
+		return nil;
+
+	fmtstrinit(&fmt);
+	sep = "";
+	for(i=0; i<v->nsx; i++){
+		if(!sxmatch(v->sx[i], "SSSS"))
+			warn("bad address: %$", v->sx[i]);
+		name = unrfc2047(nstring(v->sx[i]->sx[0]));
+		/* ignore sx[1] - route */
+		mbox = unrfc2047(nstring(v->sx[i]->sx[2]));
+		host = unrfc2047(nstring(v->sx[i]->sx[3]));
+		if(mbox == nil || host == nil){	/* rfc822 group syntax */
+			free(name);
+			free(mbox);
+			free(host);
+			continue;
+		}
+		email = esmprint("%s@%s", mbox, host);
+		free(mbox);
+		free(host);
+		fmtprint(&fmt, "%s%q %q", sep, name ? name : "", email ? email : "");
+		free(name);
+		free(email);
+		sep = " ";
+	}
+	s = fmtstrflush(&fmt);
+	if(s == nil)
+		sysfatal("out of memory");
+	return s;
+}
+
+static void
+xmsgenvelope(Msg *msg, Sx *k, Sx *v)
+{
+	hdrfree(msg->part[0]->hdr);
+	msg->part[0]->hdr = parseenvelope(v);
+}
+
+static struct {
+	char *name;
+	int offset;
+} paramtab[] = {
+	"charset",	offsetof(Part, charset),
+};
+
+static void
+parseparams(Part *part, Sx *v)
+{
+	int i, j;
+	char *s, *t, **p;
+	
+	if(isnil(v))
+		return;
+	if(v->nsx%2){
+		warn("bad message params: %$", v);
+		return;
+	}
+	for(i=0; i<v->nsx; i+=2){
+		s = nstring(v->sx[i]);
+		t = nstring(v->sx[i+1]);
+		for(j=0; j<nelem(paramtab); j++){
+			if(cistrcmp(paramtab[j].name, s) == 0){
+				p = (char**)((char*)part+paramtab[j].offset);
+				free(*p);
+				*p = t;
+				t = nil;
+				break;
+			}
+		}
+		free(s);
+		free(t);
+	}			
+}
+
+static void
+parsestructure(Part *part, Sx *v)
+{
+	int i;
+	char *s, *t;
+	
+	if(isnil(v))
+		return;
+	if(v->type != SxList){
+	bad:
+		warn("bad structure: %$", v);
+		return;
+	}
+	if(islist(v->sx[0])){
+		/* multipart */
+		for(i=0; i<v->nsx && islist(v->sx[i]); i++)
+			parsestructure(partcreate(part->msg, part), v->sx[i]);
+		free(part->type);
+		if(i != v->nsx-1 || !isstring(v->sx[i])){
+			warn("bad multipart structure: %$", v);
+			part->type = estrdup("multipart/mixed");
+			return;
+		}
+		s = nstring(v->sx[i]);
+		strlwr(s);
+		part->type = esmprint("multipart/%s", s);
+		free(s);
+		return;
+	}
+	/* single part */
+	if(!isstring(v->sx[0]) || v->nsx < 2)
+		goto bad;
+	s = nstring(v->sx[0]);
+	t = nstring(v->sx[1]);
+	strlwr(s);
+	strlwr(t);
+	free(part->type);
+	part->type = esmprint("%s/%s", s, t);
+	if(v->nsx < 7 || !islist(v->sx[2]) || !isstring(v->sx[3]) 
+	|| !isstring(v->sx[4]) || !isstring(v->sx[5]) || !isnumber(v->sx[6]))
+		goto bad;
+	parseparams(part, v->sx[2]);
+	part->idstr = nstring(v->sx[3]);
+	part->desc = nstring(v->sx[4]);
+	part->encoding = nstring(v->sx[5]);
+	part->size = v->sx[6]->number;
+	if(strcmp(s, "message") == 0 && strcmp(t, "rfc822") == 0){
+		if(v->nsx < 10 || !islist(v->sx[7]) || !islist(v->sx[8]) || !isnumber(v->sx[9]))
+			goto bad;
+		part->hdr = parseenvelope(v->sx[7]);
+		parsestructure(partcreate(part->msg, part), v->sx[8]);
+		part->lines = v->sx[9]->number;
+	}
+	if(strcmp(s, "text") == 0){
+		if(v->nsx < 8 || !isnumber(v->sx[7]))
+			goto bad;
+		part->lines = v->sx[7]->number;
+	}
+}
+
+static void
+xmsgbody(Msg *msg, Sx *k, Sx *v)
+{
+	if(v->type != SxList){
+		warn("bad body: %$", v);
+		return;
+	}
+	/*
+	 * To follow the structure exactly we should
+	 * be doing this to partcreate(msg, msg->part[0]),
+	 * and we should leave msg->part[0] with type message/rfc822,
+	 * but the extra layer is redundant - what else would be in a mailbox?
+	 */
+	parsestructure(msg->part[0], v);
+	if(msg->box->maxseen < msg->imapid)
+		msg->box->maxseen = msg->imapid;
+	if(msg->imapuid >= msg->box->uidnext)
+		msg->box->uidnext = msg->imapuid+1;
+	msgplumb(msg, 0);
+}
+
+static void
+xmsgbodydata(Msg *msg, Sx *k, Sx *v)
+{
+	int i;
+	char *name, *p;
+	Part *part;
+	
+	name = k->data;
+	name += 5;	/* body[ */
+	p = strchr(name, ']');
+	if(p)
+		*p = 0;
+	
+	/* now name is something like 1 or 3.2.MIME - walk down parts from root */
+	part = msg->part[0];
+
+	while('1' <= name[0] && name[0] <= '9'){
+		i = strtol(name, &p, 10);
+		if(*p == '.')
+			p++;
+		else if(*p != 0){
+			warn("bad body name: %$", k);
+			return;
+		}
+		if((part = subpart(part, i-1)) == nil){
+			warn("unknown body part: %$", k);
+			return;
+		}
+		name = p;
+	}
+
+	if(cistrcmp(name, "") == 0){
+		free(part->raw);
+		part->raw = nstring(v);
+		nocr(part->raw);
+	}else if(cistrcmp(name, "HEADER") == 0){
+		free(part->rawheader);
+		part->rawheader = nstring(v);
+		nocr(part->rawheader);
+	}else if(cistrcmp(name, "MIME") == 0){
+		free(part->mimeheader);
+		part->mimeheader = nstring(v);
+		nocr(part->mimeheader);
+	}else if(cistrcmp(name, "TEXT") == 0){
+		free(part->rawbody);
+		part->rawbody = nstring(v);
+		nocr(part->rawbody);
+	}
+}
+
+/*
+ * Table-driven OK info parser.
+ */
+static void xokuidvalidity(Imap*, Sx*);
+static void xokpermflags(Imap*, Sx*);
+static void xokunseen(Imap*, Sx*);
+static void xokreadwrite(Imap*, Sx*);
+static void xokreadonly(Imap*, Sx*);
+
+struct {
+	char *name;
+	char fmt;
+	void (*fn)(Imap*, Sx*);
+} oktab[] = {
+	"UIDVALIDITY", 'N',	xokuidvalidity,
+	"PERMANENTFLAGS", 'L',	xokpermflags,
+	"UNSEEN", 'N',	xokunseen,
+	"READ-WRITE", 0,	xokreadwrite,
+	"READ-ONLY",	0, xokreadonly,
+};
+
+static void
+xok(Imap *z, Sx *sx)
+{
+	int i;
+	char *name;
+	Sx *arg;
+
+	if(sx->nsx >= 4 && sx->sx[2]->type == SxAtom && sx->sx[2]->data[0] == '['){
+		if(sx->sx[3]->type == SxAtom && sx->sx[3]->data[0] == ']')
+			arg = nil;
+		else if(sx->sx[4]->type == SxAtom && sx->sx[4]->data[0] == ']')
+			arg = sx->sx[3];
+		else{
+			warn("cannot parse OK: %$", sx);
+			return;
+		}
+		name = sx->sx[2]->data+1;
+		for(i=0; i<nelem(oktab); i++){
+			if(cistrcmp(name, oktab[i].name) == 0){
+				if(oktab[i].fmt && (arg==nil || arg->type != fmttype(oktab[i].fmt))){
+					warn("malformed %s: %$", name, arg);
+					continue;
+				}
+				oktab[i].fn(z, arg);
+			}
+		}
+	}
+}
+
+static void
+xokuidvalidity(Imap *z, Sx *sx)
+{
+	int i;
+	Box *b;
+	
+	if((b=z->box) == nil)
+		return;
+	if(b->validity != sx->number){
+		b->validity = sx->number;
+		b->uidnext = 1;
+		for(i=0; i<b->nmsg; i++)
+			msgfree(b->msg[i]);
+		free(b->msg);
+		b->msg = nil;
+		b->nmsg = 0;
+	}
+}
+
+static void
+xokpermflags(Imap *z, Sx *sx)
+{
+//	z->permflags = parseflags(sx);
+}
+
+static void
+xokunseen(Imap *z, Sx *sx)
+{
+//	z->unseen = sx->number;
+}
+
+static void
+xokreadwrite(Imap *z, Sx *sx)
+{
+//	z->boxmode = ORDWR;
+}
+
+static void
+xokreadonly(Imap *z, Sx *sx)
+{
+//	z->boxmode = OREAD;
+}
+
diff --git a/src/cmd/upas/nfs/imap.h b/src/cmd/upas/nfs/imap.h
new file mode 100644
index 0000000..c4677a9
--- /dev/null
+++ b/src/cmd/upas/nfs/imap.h
@@ -0,0 +1,23 @@
+typedef struct Imap Imap;
+
+void		imapcheckbox(Imap *z, Box *b);
+Imap*		imapconnect(char *server, int mode);
+int		imapcopylist(Imap *z, char *nbox, Msg **m, uint nm);
+void		imapfetchraw(Imap *z, Part *p);
+void		imapfetchrawbody(Imap *z, Part *p);
+void		imapfetchrawheader(Imap *z, Part *p);
+void		imapfetchrawmime(Imap *z, Part *p);
+int		imapflaglist(Imap *z, int op, int flag, Msg **m, uint nm);
+void		imaphangup(Imap *z, int ticks);
+int		imapremovelist(Imap *z, Msg **m, uint nm);
+int		imapsearchbox(Imap *z, Box *b, char *search, Msg ***mm);
+
+extern	int	chattyimap;
+
+enum
+{
+	Unencrypted,
+	Starttls,
+	Tls,
+	Cmd
+};
diff --git a/src/cmd/upas/nfs/main.c b/src/cmd/upas/nfs/main.c
new file mode 100644
index 0000000..aa37d8b
--- /dev/null
+++ b/src/cmd/upas/nfs/main.c
@@ -0,0 +1,68 @@
+/*
+TO DO
+
+can get disposition info out of imap extended structure if needed
+sizes in stat/ls ?
+translate character sets in =? subjects
+
+fetch headers, bodies on demand
+
+cache headers, bodies on disk
+
+cache message information on disk across runs
+
+body.jpg
+
+*/
+
+#include "a.h"
+
+Imap *imap;
+
+void
+usage(void)
+{
+	fprint(2, "usage: mailfs [-t] server\n");
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	char *server;
+	int mode;
+
+	mode = Unencrypted;
+	ARGBEGIN{
+	default:
+		usage();
+	case 'D':
+		chatty9p++;
+		break;
+	case 'V':
+		chattyimap++;
+		break;
+	case 't':
+		mode = Tls;
+		break;
+	case 'x':
+		mode = Cmd;
+		break;
+	}ARGEND
+
+	quotefmtinstall();	
+	fmtinstall('$', sxfmt);
+
+	if(argc != 1)
+		usage();
+	server = argv[0];
+	
+	mailthreadinit();
+	boxinit();
+	fsinit0();
+
+	if((imap = imapconnect(server, mode)) == nil)
+		sysfatal("imapconnect: %r");
+	threadpostmountsrv(&fs, "mail", nil, 0);
+}
+
diff --git a/src/cmd/upas/nfs/mbox.c b/src/cmd/upas/nfs/mbox.c
new file mode 100644
index 0000000..5f90f0a
--- /dev/null
+++ b/src/cmd/upas/nfs/mbox.c
@@ -0,0 +1,68 @@
+#include "a.h"
+
+Mailbox *hash[123];
+Mailbox **box;
+uint nbox;
+
+static void
+markboxes(int mark)
+{
+	Mailbox *b;
+	
+	for(i=0; i<nbox; i++)
+		if(box[i])
+			box[i]->mark = mark;
+}
+
+static void
+sweepboxes(void)
+{
+	Mailbox *b;
+	
+	for(i=0; i<nbox; i++)
+		if(box[i] && box[i]->mark){
+			freembox(box[i]);
+			box[i] = nil;
+		}
+}
+
+static Mailbox*
+mboxbyname(char *name)
+{
+	int i;
+	
+	for(i=0; i<nbox; i++)
+		if(box[i] && strcmp(box[i]->name, name) == 0)
+			return box[i];
+	return nil;
+}
+
+static Mailbox*
+mboxbyid(int id)
+{
+	if(id < 0 || id >= nbox)
+		return nil;
+	return box[id];
+}
+
+static Mailbox*
+mboxcreate(char *name)
+{
+	Mailbox *b;
+	
+	b = emalloc(sizeof *b);
+	b->name = estrdup(name);
+	if(nbox%64 == 0)
+		box = erealloc(box, (nbox+64)*sizeof box[0]);
+	box[nbox++] = b;
+	return b;
+}
+
+void
+mboxupdate(void)
+{
+	markboxes();
+	if(imapcmd("LIST \"\" *") < 0)
+		return;
+	sweepboxes();
+}
diff --git a/src/cmd/upas/nfs/mkfile b/src/cmd/upas/nfs/mkfile
new file mode 100644
index 0000000..7716ab5
--- /dev/null
+++ b/src/cmd/upas/nfs/mkfile
@@ -0,0 +1,18 @@
+<$PLAN9/src/mkhdr
+
+TARG=mailfs
+
+OFILES=\
+	box.$O\
+	decode.$O\
+	fs.$O\
+	imap.$O\
+	main.$O\
+	sx.$O\
+	thread.$O\
+	util.$O\
+
+HFILES=a.h box.h imap.h sx.h
+
+<$PLAN9/src/mkone
+
diff --git a/src/cmd/upas/nfs/msg.c b/src/cmd/upas/nfs/msg.c
new file mode 100644
index 0000000..f4a2686
--- /dev/null
+++ b/src/cmd/upas/nfs/msg.c
@@ -0,0 +1,9 @@
+#include "a.h"
+
+something about a cache of msgs here
+
+cache flushes optionally to disk
+	before being tossed out
+
+reload from disk, then from server
+
diff --git a/src/cmd/upas/nfs/sx.c b/src/cmd/upas/nfs/sx.c
new file mode 100644
index 0000000..65d338c
--- /dev/null
+++ b/src/cmd/upas/nfs/sx.c
@@ -0,0 +1,217 @@
+#include "a.h"
+
+Sx *Brdsx1(Biobuf*);
+
+Sx*
+Brdsx(Biobuf *b)
+{
+	Sx **sx, *x;
+	int nsx;
+	
+	nsx = 0;
+	sx = nil;
+	while((x = Brdsx1(b)) != nil){
+		sx = erealloc(sx, (nsx+1)*sizeof sx[0]);
+		sx[nsx++] = x;
+	}
+	x = emalloc(sizeof *x);
+	x->sx = sx;
+	x->nsx = nsx;
+	x->type = SxList;
+	return x;
+}
+
+int 
+sxwalk(Sx *sx)
+{
+	int i, n;
+	
+	if(sx == nil)
+		return 1;
+	switch(sx->type){
+	default:
+	case SxAtom:
+	case SxString:
+	case SxNumber:
+		return 1;
+	case SxList:
+		n = 0;
+		for(i=0; i<sx->nsx; i++)
+			n += sxwalk(sx->sx[i]);
+		return n;
+	}
+}
+
+void
+freesx(Sx *sx)
+{
+	int i;
+	
+	if(sx == nil)
+		return;
+	switch(sx->type){
+	case SxAtom:
+	case SxString:
+		free(sx->data);
+		break;
+	case SxList:
+		for(i=0; i<sx->nsx; i++)
+			freesx(sx->sx[i]);
+		free(sx->sx);
+		break;
+	}
+	free(sx);
+}
+
+Sx*
+Brdsx1(Biobuf *b)
+{
+	int c, len, nbr;
+	char *s;
+	vlong n;
+	Sx *x;
+	
+	c = Bgetc(b);
+	if(c == ' ')
+		c = Bgetc(b);
+	if(c < 0)
+		return nil;
+	if(c == '\r')
+		c = Bgetc(b);
+	if(c == '\n')
+		return nil;
+	if(c == ')'){	/* end of list */
+		Bungetc(b);
+		return nil;
+	}
+	if(c == '('){	/* parenthesized list */
+		x = Brdsx(b);
+		c = Bgetc(b);
+		if(c != ')')	/* oops! not good */
+			Bungetc(b);
+		return x;
+	}
+	if(c == '{'){	/* length-prefixed string */
+		len = 0;
+		while((c = Bgetc(b)) >= 0 && isdigit(c))
+			len = len*10 + c-'0';
+		if(c != '}')	/* oops! not good */
+			Bungetc(b);
+		c = Bgetc(b);
+		if(c != '\r')	/* oops! not good */
+			;
+		c = Bgetc(b);
+		if(c != '\n')	/* oops! not good */
+			;
+		x = emalloc(sizeof *x);
+		x->data = emalloc(len+1);
+		if(Bread(b, x->data, len) != len)
+			;	/* oops! */
+		x->data[len] = 0;
+		x->ndata = len;
+		x->type = SxString;
+		return x;
+	}
+	if(c == '"'){	/* quoted string */
+		s = nil;
+		len = 0;
+		while((c = Bgetc(b)) >= 0 && c != '"'){
+			if(c == '\\')
+				c = Bgetc(b);
+			s = erealloc(s, len+1);
+			s[len++] = c;
+		}
+		s = erealloc(s, len+1);
+		s[len] = 0;
+		x = emalloc(sizeof *x);
+		x->data = s;
+		x->ndata = len;
+		x->type = SxString;
+		return x;
+	}
+	if(isdigit(c)){	/* number */
+		n = c-'0';;
+		while((c = Bgetc(b)) >= 0 && isdigit(c))
+			n = n*10 + c-'0';
+		Bungetc(b);
+		x = emalloc(sizeof *x);
+		x->number = n;
+		x->type = SxNumber;
+		return x;
+	}
+	/* atom */
+	len = 1;
+	s = emalloc(1);
+	s[0] = c;
+	nbr = 0;
+	while((c = Bgetc(b)) >= 0 && c > ' ' && !strchr("(){}", c)){ 
+		/* allow embedded brackets as in BODY[] */
+		if(c == '['){
+			if(s[0] == '[')
+				break;
+			else
+				nbr++;
+		}
+		if(c == ']'){
+			if(nbr > 0)
+				nbr--;
+			else
+				break;
+		}
+		s = erealloc(s, len+1);
+		s[len++] = c;
+	}
+	if(c != ' ')
+		Bungetc(b);
+	s = erealloc(s, len+1);
+	s[len] = 0;
+	x = emalloc(sizeof *x);
+	x->type = SxAtom;
+	x->data = s;
+	x->ndata = len;
+	return x;		
+}
+
+int
+sxfmt(Fmt *fmt)
+{
+	int i, paren;
+	Sx *sx;
+	
+	sx = va_arg(fmt->args, Sx*);
+	if(sx == nil)
+		return 0;
+
+	switch(sx->type){
+	case SxAtom:
+	case SxString:
+		return fmtprint(fmt, "%q", sx->data);
+
+	case SxNumber:
+		return fmtprint(fmt, "%lld", sx->number);
+
+	case SxList:
+		paren = !(fmt->flags&FmtSharp);
+		if(paren)
+			fmtrune(fmt, '(');
+		for(i=0; i<sx->nsx; i++){
+			if(i)
+				fmtrune(fmt, ' ');
+			fmtprint(fmt, "%$", sx->sx[i]);
+		}
+		if(paren)
+			return fmtrune(fmt, ')');
+		return 0;
+
+	default:
+		return fmtstrcpy(fmt, "?");
+	}
+}
+
+int
+oksx(Sx *sx)
+{
+	return sx->nsx >= 2 
+		&& sx->sx[1]->type == SxAtom 
+		&& cistrcmp(sx->sx[1]->data, "OK") == 0;
+}
diff --git a/src/cmd/upas/nfs/sx.h b/src/cmd/upas/nfs/sx.h
new file mode 100644
index 0000000..910f002
--- /dev/null
+++ b/src/cmd/upas/nfs/sx.h
@@ -0,0 +1,31 @@
+/*
+ * S-expressions as used by IMAP.
+ */
+
+enum
+{
+	SxUnknown = 0,
+	SxAtom,
+	SxString,
+	SxNumber,
+	SxList,
+};
+
+typedef struct Sx Sx;
+struct Sx
+{
+	int type;
+	char *data;
+	int ndata;
+	vlong number;
+	Sx **sx;
+	int nsx;
+};
+
+Sx*	Brdsx(Biobuf*);
+Sx*	Brdsx1(Biobuf*);
+void	freesx(Sx*);
+int	oksx(Sx*);
+int	sxfmt(Fmt*);
+int	sxwalk(Sx*);
+
diff --git a/src/cmd/upas/nfs/thread.c b/src/cmd/upas/nfs/thread.c
new file mode 100644
index 0000000..710a33d
--- /dev/null
+++ b/src/cmd/upas/nfs/thread.c
@@ -0,0 +1,37 @@
+#include "a.h"
+
+typedef struct New New;
+struct New
+{
+	void (*fn)(void*);
+	void *arg;
+};
+
+Channel *mailthreadchan;
+
+void
+mailthread(void (*fn)(void*), void *arg)
+{
+	New n;
+	
+	n.fn = fn;
+	n.arg = arg;
+	send(mailthreadchan, &n);
+}
+
+void
+mailproc(void *v)
+{
+	New n;
+	
+	while(recv(mailthreadchan, &n) == 1)
+		threadcreate(n.fn, n.arg, STACK);
+}
+
+void
+mailthreadinit(void)
+{
+	mailthreadchan = chancreate(sizeof(New), 0);
+	proccreate(mailproc, nil, STACK);
+}
+
diff --git a/src/cmd/upas/nfs/util.c b/src/cmd/upas/nfs/util.c
new file mode 100644
index 0000000..ac9deba
--- /dev/null
+++ b/src/cmd/upas/nfs/util.c
@@ -0,0 +1,13 @@
+#include "a.h"
+
+void
+warn(char *fmt, ...)
+{
+	va_list arg;
+	
+	va_start(arg, fmt);
+	fprint(2, "warning: ");
+	vfprint(2, fmt, arg);
+	fprint(2, "\n");
+	va_end(arg);
+}