imap-based new upas/fs
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;
+}
+